I did a little simple refactoring in the previous article, just some renaming and method extraction, and got away with it. I was running the tests, but I had forgotten that none of them tested the TextModel that I was working on. No excuse, sir: we should have written the tests first, but sometimes even we nod. We've decided that these articles won't be sanitized: you'll see us learning, making mistakes, and -- we hope -- ultimately producing some pretty nice code.
We're still working on our first story:
We have this functionality working in the GUI, and therefore in the TextModel, which drives the GUI. We should have written tests for the TextModel already, but we couldn't figure out how. Now we know enough to do it, and we sorely need to write those tests. We'll start with simple ones, and move forward.
Our first test just checks to make sure that a TextModel that hasn't actually done anything answers an empty array of strings. Our purpose with this test was mostly to check that we are correctly set up.
[SetUp] public void CreateModel() {
model = new TextModel();
}
[Test] public void TestNoLines() {
model.Lines = new String[0];
AssertEquals(0, model.Lines.Length);
}
Next we test that a TextModel given three lines, with no processing, returns three lines. Again, a pretty simple test. We like to go in tiny steps, especially since these tests usually run, giving us a little jolt of success. Even so, once in a while we get a surprise. In addition, remember that we're just learning C#. We hadn't worked much with arrays of strings and such, so we take extra small steps, knowing that we're likely to write code that doesn't work. Anyway, here's the no-processing test:
[Test] public void TestNoProcessing() {
model.Lines = new String[3] { "hi", "there", "chet"};
AssertEquals(3, model.Lines.Length);
}
OK, so far so good. Now we'll actually do some work. This test takes a TextModel with one line of content, "hello world", and tells it to insert a paragraph tag. We expect to find three lines in total: the original, a blank line, and a paragraph tag pair. The cursor should be between the P's of the P tag. That's 18 characters in: 11 for hello world + 2 for newline + 2 for blank line + 3 for *ltP>.
[Test] public void TestOneEnter() {
model.Lines = new String[1] {"hello world" };
model.SelectionStart = 5;
model.InsertParagraphTag();
AssertEquals(3, model.Lines.Length);
AssertEquals(18, model.SelectionStart);
}
Because we're confident in the content, we're not checking it. A case could be made that we should be checking it, In a later test, we actually do that. As always, it's a judgment call what to test when you're testing after the fact. Testing before the fact usually gives us better tests. Try it both ways yourself and see what you discover.
We implemented a new feature -- maybe I'll show you the code for it later -- that handled the special case of hitting enter in a TextBox containing nothing. It just inserts a single paragraph tag pair (not the blank line). Here's the test we wrote for that:
[Test] public void TestEmptyText() {
model.Lines = new String[0];
model.InsertParagraphTag();
AssertEquals(1, model.Lines.Length);
AssertEquals(3, model.SelectionStart);
}
This test didn't work, since it was a new feature, and it took us a little while to make it go, because we had a subtle defect in our code that figures out which line the selection is on. It turns out that if the cursor was at the beginning of a line, our code thought it was in the previous line. There were a couple of offsetting errors making that happen, and it took us some time to find the second problem.
Remember all those comments in the TextModel code, that we're here to remove. As we worked to implement this test's feature, Chet remarked that the comments weren't helping him understand the code. Even in its current ugly state, he would rather look at the code without comments. So would I. Maybe so would you. When we get around to showing you the refactorings, we're pretty sure you won't want very many comments.
We wrote another test, to handle the cursor being at the start of the line. It looks like this:
[Test] public void InsertWithCursorAtLineStart () {
model.Lines = new String[3] { "<P>one</P>", "", "<P>two</P>"};
model.SelectionStart = 14;
model.InsertParagraphTag();
AssertEquals("<P>two</P>", model.Lines[2]);
}
In this test, the cursor is ahead of the P tag on line "two". We had discovered while working on the test above that when we hit enter there, the blank line and paragraph tag pair was inserted in front of the "two" line, instead of after it as we suspected. So we wrote this test in preparation for fixing the problem.
This is a standard Extreme Programming recommendation, by the way: if you find a defect, first write a test that will work when the defect is fixed, and only then, fix the defect. There are at least three advantages to this idea:
I think it's this last item that really makes it worth while. By having the discipline to write this test and fix it, we have a sharper focus on what kinds of situations we make mistakes in, and on the kind of test we should write to avoid those mistakes. We always try to reflect just a bit: "What have we learned from this", but even if we forget, the little ritual pounds a little sense into our heads. Try this practice out: we think you'll like it.
This test gave us a bit of an extra problem: when I originally counted the characters to figure out where the selection start should be, I got 13. So we thought the code was wrong, changed it back to the way it had been before the TestEmptyText, then finally figured out that our test was wrong by printing the output and counting it again.
Still, all this rigmarole took us only about an hour, and now we feel that the TextModel is pretty well protected for further refactoring to express its ideas better. We'll proceed to do that in the next article.