Wednesday, 15 August 2007
Adventures in test-driven development Part 4: What's next?
1. Test-first: write a test, watch it fail, write the simplest (production) code to make the test pass (you can even hardcore some constants in your code), then refactor in smallest possible steps while keeping the test (and the other tests) pass. This is referred to as Red-Green-Refactor cycle.
2. Test-driven: when your testing is too complex, don't just sigh and accept it. This is a sign that your design can be improved. Let the testing drive your code.
3. YAGNI -- you ain't gonna need it. Instead of implementing a Good Thing just because it is one of the GoF patterns or you've just read it in somebody's blog, whatever makes your tests pass is OK. You see that this method you just wrote is ugly, but wait until the next test drives you in the right direction for refactoring. There's another similar principle, KISS (Keep It Simple, Stupid).
4. Separation of Concerns (SoC) principle -- each object has to have a distinct responsibility. Keep in mind, though, that this is just a clue: whenever you have problems with testability and you don't know what to do, try this or some other design idea (see Dependency Injection below) and see if it simplifies things. The general idea is that you don't apply these patterns or principles blindly, but let your tests decide what to apply.
5. Dependency Injection. We have seen it when we were getting rid of the Graphics dependency. The main idea is that it is very hard to test your class when there's an external dependency involved, such as an SMTP server, local file system, or some unmanaged stuff. So, don't hardcore it into your component, have it "injected" somehow in your production code, and mock it in your test code to make sure that your code interacts correctly with it. Again, the "weakness" of our mock framework dictated this. But this is not just designing for testability -- turns out that it opens a possibility for other reporting options -- exporting to HTML, Word, or maybe projecting onto a Star Wars style transparent screen.
This is no accident that Rhino Mocks forced us for a better design. While TypeMock does its job by applying some sort of black magic, Rhino plays by the .Net rules: overrides virtual methods, extends classes, implements interfaces -- does what you or users of your component would do to extend it.
So, while I'm not going to publish all the details, I'm going to share all my big moves and discoveries, especially TDD-related. The latest source of FreshReports can be found in the SVN repository at SourceForge at https://freshreports.svn.sourceforge.net/svnroot/freshreports/trunk.
Further reading:
TDD Design Starter Kit by Jeremy D. Miller
Achieving And Recognizing Testable Software Designs by Roy Osherove
testdrivendevelopment group on Yahoo
Friday, 10 August 2007
Adventures in test-driven development Part 3: Getting Rid Of the Ugly Stuff
In order to test our behavior and not print something in the process, we need to mock the Graphics object. However, it is impossible with Rhino, since the class is sealed, or NotInheritable. So, somehow we should eliminate this hardcoded dependency from our class.
Why is this good, apart from testability? First, the Graphics class is a heavy dependency. The sole fact that it is IDisposable tells us that it depends on some unmanaged resources. In addition, it brings a dependency on Win32, which is bad, since we want to develop a universal component. Sure if we want to use our component on Mono or whatever CLR implementation exists, we could find a Graphics analog, but there's no guarantee that these implementations, that are not part of the standards, are implemented the same way.
Also, it carries too much with it that we just don't need. We don't want our main object perform all the low-level work, we want to serve it as a main controller, leaving all individual details to other objects.
Therefore, we are going to introduce a Canvas class, which is going to serve as our main printing device. Immediately, we have two advantages. First, we can provide multiple inheritors of the Canvas class, so, in theory, we can print to PDF, HTML, Word, anything. Second, we are ruling the interface of our Canvas class. Whenever we look at it, we immediately know its responsibilities within our project. We don't have to adapt to the Graphics interface (this is done in the implementation). Since we don't need mysterious stuff that Graphics has, like GetHdc() or FromHwndInternal(), we don't include it as part of our class, so those who use our component won't be tempted to play with it.
Enough said, let's code! Before we even start with our second test, we should do a little redesign.We should move in small steps, running our test after each step. This way we immediately know if we make a wrong step. Remember that RhinoMocks requires that we mock an interface or an inheritable class. The first step is to construct a thin wrapper around the Graphics class, giving out just the methods we need. Let's call it GraphicsCanvas, and let's put it into WinForms namespace. This is the only namespace that is going to reference the System.Drawing assembly, and eventually we are going to extract it into a separate project. Although it seems like adding complexity, actually it makes our design cleaner, since the project itself becomes clean of "the earthly stuff" in some way. For the same reason, I'm going to introduce my analog of the System.Drawing.Point structure.
The first step is, unfortunately, relatively big. We have to write some basic stuff into our new class so that we have something to test. Let's have it:
Namespace WinForms
Public Class GraphicsCanvas
Private _graphics As Drawing.Graphics
Private Sub New()
End Sub
Public Sub New(ByVal graphics As System.Drawing.Graphics)
Me._graphics = graphics
End Sub
Sub DrawString(ByVal text As String, ByVal position As Printing.Point)
Me._graphics.DrawString(text, New Drawing.Font("Verdana", 8.0, Drawing.FontStyle.Regular), Drawing.Brushes.Black, 0, 0)
End Sub
End Class
End Namespace
What do we have here? First, we hold a private variable for our Graphics object. Next, we have a private parameterless constructor, which makes it impossible to construct our class without parameters. Indeed, it cannot exist without the Graphics object. Next, we have a public constructor. I should say here that while the class itself and the constructor can reference the Graphics class, all other public methods shouldn't, since we are going to convert them into an interface later. So, the last is our first printing method. The least we should provide for printing is the text and its location. In fact, it's about 90% of our needs, or close to that.
Now, let's figure out our testing strategy. The way Rhino Mocks, as well as most other mock frameworks, work is quite different from TypeMock. TypeMock places a sort of hook on a class before the concrete object is created, so that when it is created, some method calls can be intercepted. So, it's fine that the object itself is created somewhere in our code. Just like the Graphics object, which is created somewhere deep in the framework. On the other hand, a Rhino Mocks mock should be created explicitly in the test. That forces us to redesign our code, so that we could somehow feed the mock object into our production code. This is done via constructor arguments, property setters, or method parameters. This procedure is generally called "dependency injection".
In our case, each page corresponds to a separate Graphics object, so it makes sense that we provide a separate GraphicsCanvas object for each page. So, the first refactoring is to extract the call to DrawString() to a separate method:
Protected Overrides Sub OnPrintPage(ByVal e As System.Drawing.Printing.PrintPageEventArgs)
Me.PrintCurrentPage(e.Graphics)
End Sub
Sub PrintCurrentPage(ByVal graphics As System.Drawing.Graphics)
For Each section In Me.Sections.Values
For Each element In section.Elements.Values
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
In our second test, we are not going to call the Report.Print() method, since there's no way to inject the Canvas dependency. Instead, we'll call the PrintCurrentPage, but only after we figure out how to call it. So, this method should take our wrapper as an argument. The next version looks like this:
Protected Overrides Sub OnPrintPage(ByVal e As System.Drawing.Printing.PrintPageEventArgs)
Dim canvas = New WinForms.GraphicsCanvas(e.Graphics)
Me.PrintCurrentPage(canvas)
End Sub
Sub PrintCurrentPage(ByVal canvas As WinForms.GraphicsCanvas)
For Each section In Me.Sections.Values
For Each element In section.Elements.Values
Dim Text = CType(element, Elements.LabelElement).Text
canvas.DrawString(Text, New Fresh.Printing.Point)
Next
Next
End Sub
What are the requirements for the type of the canvas argument? First, it should allow us to pass our GraphicsCanvas object, so it should be a base class. Second, it should be mockable. In addition, it should provide our base functionality (the DrawString method) without implementing it (so the method should be abstract, or MustOverride). So, it's either an abstract class or an interface. Typically, such things are done with interfaces. Perhaps historically mock frameworks could work with interfaces but not with abstract classes. For me, an interface is some common functionality among unrelated classes (like IDisposable), whereas an abstract class is a conceptual common ground, like Shape for all geometrical shapes.
So, let's shoot our Canvas class:
Namespace Printing
Public MustInherit Class Canvas
MustOverride Sub DrawString(ByVal text As String, ByVal position As Point)
End Class
End Namespace
Sub PrintCurrentPage(ByVal canvas As Printing.Canvas)So, did it make our design better? First, we have greater control over printing. We have extracted our PrintCurrentPage method and now can call it directly. This is a big step towards being independent from the PrintDocument class by the way. Next, we can print to anything we like, provided we can implement a custom Canvas class. Our main Report object has been relieved from the printing burden, and can concentrate on more important tasks (such as handling the report structure and routing commands to other objects). This is called the Separation of Responsibilities (SoC) principle.
Finally, let's see our test:
First goes some preparation. Next we have a Using mocks.Record statement -- it's a first part of the Record-Replay pattern, where we first write down what method calls we expect, what arguments should be there, and which results should the methods return. In our case, the first line states that we should call the DrawString method. The arguments are irrelevant here, but without them the code won't be compiled. The second line tells us that the first argument should be equal to our test string, and the second can be anything.<Test()> Sub TestWithRhino()
Dim mocks As New MockRepository()
Dim canvasMock = mocks.CreateMock(Of Printing.Canvas)()
Dim TestReport = New Report
Dim TestSection As New Core.Section
TestReport.Sections.Add("", TestSection)
Dim TestElement As New Elements.LabelElement
TestSection.Elements.Add("", TestElement)
TestElement.Text = "test"
Using mocks.Record
canvasMock.DrawString("", New Printing.Point()) 'we can provide any arguments here
LastCall.Constraints(Rhino.Mocks.Constraints.Text.Like("test"), New Rhino.Mocks.Constraints.Anything)
End Using
Using mocks.Playback
TestReport.PrintCurrentPage(canvasMock)
End Using
End Sub
Last, we have a Playback block where we put our tested method.
Our test actually verifies that the appropriate call is made to the Canvas object. It doesn't verify that the string is actually printed. This is where Rhino Mocks can't help us. However, we can easily write a separate test (using TypeMock) for it. Since we have two separate objects, we can test them independently. We can do all kinds of tests verifying that DrawString is invoked correctly, and only one test verifying that it actually prints something. When we have other Canvas objects, we'll have to make one test for each object, instead of testing all possible report layouts and data with each canvas. That's what the term unit testing is about.
Another thing that's not tested here is that calling the Print() method actually calls the PrintCurrentPage method. So far, we have our first test to verify it, but this is an indication that this piece has to be refactored as well. I guess I'll be making a separate class ReportPrinter that inherits from PrintDocument and manages the interaction with the actual printing and previewing (it can even be used at design-time in a form), and our Report class will be completely ignorant of these implementation details. However, I'll wait till I implement paging and let my tests drive my design.
Sunday, 5 August 2007
Adventures in test-driven development part 2: Getting a Meaningful Result
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
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:<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
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
Next time we are going to make a huge refactoring, since it's going to make our test-driven life a lot easier.