Sunday, 5 August 2007

Adventures in test-driven development part 2: Getting a Meaningful Result

Last time we forced out component to print a word and satisfy out first test. However, we didn't finish the proper TDD cycle: red-green-refactor. So, how can we refactor our code so that it would become a sample of perfect design?

I'm tempted to eliminate the ugly Graphics dependency that's making our code hard to test (even with TypeMock). But after reading some posts on Behavior Driven Development, I feel that I should focus on the main part of our component -- the customer who's using it.

So, I'll start with a user story. Since I'm the author, I'm trying to figure out what would a client want. Something like that:

1. I want to create simple units (called Elements) that represent a single printed unit, like a string or a line, so that a client could add, remove, and move around these elements at her will.
2. I want to organize these elements into containers (called Sections), so that a client could easily think in terms of data records (corresponding to objects or, God forbid, database rows). So, the sections would allow her to group elements in terms of data (single section corresponding to a single data source object) or presentation (page headers/footers).
3. I want to attach unique IDs to all elements and sections so that the client could easily identify each element, at least within a section.

There is another problem with my test. I didn't put the text "test" that's being printed. So, the test actually tests that the word "test" is printed always. This is clearly not my intent, so I should rewrite it. Remember that it should fail, so just to make sure I change the expected word "test" to "test2".

I'm going to name my first element class "LabelElement", since its behavior is close to that of a label. I'm going to add a Sections property to my Report class to hold the list of sections. Same with the Elements property for the Section class. Both properties are going to of be respective generic Dictionary types. It seems like I'm making a lot of design decisions even before writing the test, but I need that to make it compile.

So, my new test code is

<Test(), VerifyMocks()> Sub PrintSimpleText()

Dim graphicsMock = MockManager.Mock(Of Drawing.Graphics)(Constructor.Mocked)

graphicsMock.ExpectCall("DrawString").Args("test2", Check.IsAny, Check.IsAny, Check.IsAny, Check.IsAny)

Dim SystemFontsMock = TypeMock.MockManager.Mock(GetType(System.Drawing.SystemFonts))

SystemFontsMock.ExpectGetAlways("DefaultFont", New System.Drawing.Font("Verdana", 8))

Dim TestReport = New Report

Dim TestSection As New Core.Section

TestReport.Sections.Add("", TestSection) 'we'll use the first argument for the ID, let's just have an empty string here for now

Dim TestElement As New Elements.LabelElement

TestSection.Elements.Add("", TestElement)

TestElement.Text = "test2"

TestReport.PrintController = New Drawing.Printing.PreviewPrintController

TestReport.Print()

End Sub

Of course, my test fails. It prints "test" instead of "test2". So, for the Green phase I just shamelessly change the hardcoded value to "test2". Then I get to the Refactor stage, and the code becomes something like this:

Protected Overrides Sub OnPrintPage(ByVal e As System.Drawing.Printing.PrintPageEventArgs)

For Each section In Me.Sections.Values

For Each element In section.Elements.Values

e.Graphics.DrawString(CType(element, Elements.LabelElement).Text, New Drawing.Font("Verdana", 8.0, Drawing.FontStyle.Regular), Drawing.Brushes.Black, 0, 0)

Next

Next

End Sub

You can see that we actually achieved more than we intended. We can print several elements now -- all elements that are added to the Report. While we shouldn't add more than one feature at a time usually, this one came almost as a side effect -- I should have printed only the first element of the first section, but it turned out that the code is simpler this way.

Next time we are going to make a huge refactoring, since it's going to make our test-driven life a lot easier.

No comments: