Thursday, March 8, 2007

Andrzej on Test Driven Development

Test Driven Development helps me creating better software. TDD is not only about testing. It's more about designing and managing scope. I'll try to show what I mean by using a simple example based on tabbedimages application.

Tabbedimages is a simple image viewer. We are going to add drag and drop feature to it.
I start with a requirement, which I analyze and spike for a working solution. Based on the analysis I create a user story and an automated acceptance test. The acceptance test "drives" me when I'll add unit tests and the production code.
  • Requirement
    • tabbedimages is a simple image viewer.
    • We want to add a new feature to tabbedimages.
  • Title.
    • 'Drag&drop support'.
    • TIP: It's good to have a short title for a user story.
  • Analysis: let's list things to worry about:
    • Single image files
    • Multiple image files
    • Directories
    • Non-image files
    • Already open image.
  • Spiking
    • TIP: This part should give us better understanding of the problem
      • If we're sure how to implement the new feature we can skip this step.
    • tabbedimages is implemented using IronPython and Windows Forms.
      • The code is availalable here.
    • Google for 'drag and drop windowsforms' and see some code examples.
    • Check out the fresh version of tabbedimages.
    • Try to add the required feature to our code base (without tests).
    • Discover that Windows Forms has support for DragDropEffect.
      • Which displays a 'plus' sign if the thing that we're trying to drag is acceptable.
    • Add the DragDropEffect to the list of things to worry about.
  • User story:
    • Marten wants to drag and drop his images from his desktop to tabbedimages.
    • He starts tabbedimages.
    • He then drags the 'Faye001.jpg' file over the application.
    • The plus sign appears.
    • He drops it.
    • A new tab is created with a label saying 'Faye001.jpg'
    • 'She's cute' he thinks
    • Marten realizes that there are more Faye's pictures.
    • He drags 'Faye001.jpg' (again) and 'Faye002.jpg'.
    • He drops them.
    • Two new tabs are created.
    • He drags and drops readme.txt file.
    • The message box appears saying 'readme.txt doesn't appear to be a valid image file'
    • He quits tabbedimages.
  • Functional test:
    • Write the ideal code (DSL-like) that follows the user story steps:
    • marten.starts()
      he.asserts_number_of_tabs(0)
      he.drags('Faye001.jpg')
      assert shows_plus()
      he.drops('Faye001.jpg')
      he.asserts_number_of_tabs(1)
      he.asserts_tab_labels(['Faye001.jpg'])
      fayes_pictures = ['Faye001.jpg', 'Faye002.jpg']
      marten.drags_and_drops(fayes_pictures)
      marten.drops(fayes_pictures)
      he.asserts_number_of_tabs(3)
      he.asserts_tab_labels(['Faye001.jpg', Faye001.jpg', Faye002.jpg'])
      he.drags_and_drops('readme.txt')
      he.sees_message_box("readme.txt doesn't appear to be a valid image file")
      he.quits()
  • Implementation
    • Run the Functional Test (FT).
    • Whenever FT fails or you can think of any edge cases not covered by FT:
      • Write appropriate Unit Test that reflects the problem.
      • Add the implementation to fix the problem.
After the last phase the drag & drop feature should be ready to use. Of course, this example presented a very simple problem. Hopefully, the Test Driven Development as shown here should be easy to understand. In the near future, I'll try to write more about the implementation phase, including some refactoring techniques, so stay tuned!

2 comments:

Grig Gheorghiu said...

Andrzej -- very nice! You should aggregate your blog into Planet Python (or at least the posts tagged with Python, which you can do these days with Blogger). Fuzzyman sometimes links to your posts, but having your feed on PlanetPython would be even better IMO.

Grig

Bjorn said...

Nice explanation. I like how to clarify what happens in the spiking part.

Have you considered using doctests to express the unit tests? I find doctests offers a much more natural expression of what you expect to happen, as if you were doing it interactively in an interpreter, without having to invent new assert_* methods, and makes it easy to mix textual description and code.

For example:

...
>>> len(tabs())
2
>>> tabs()
['Faye001.jpg', 'Faye002.jpg']
...