Exploration Through ExampleExample-driven development, Agile testing, context-driven testing, Agile programming, Ruby, and other things of interest to Brian Marick
|
Fri, 15 Apr 2005Not exactly, but two species of octopus walk - nay, scurry - on two legs. Video here.
## Posted at 10:53 in category /misc
[permalink]
[top]
Design-Driven Test-Driven Design (Part 4) For background, see the table of contents in the right sidebar. The story so far: I got a Fit test describing a user workflow through a medical records system. I made part of that workflow work using a Model View Presenter style and Mock Views. I'm now ready to start building the real views that will talk to the Macintosh's GUI framework. Here's my normal practice when driving coding with Fit business-facing examples: I constantly want to make the next cell of the Fit table green. When I know that will take too long for comfort, I divide-and-conquer the problem by breaking the interval into shorter ones punctuated by xUnit green bars. There are some disadvantages to treating xUnit tests as secondary to business-facing tests, but that's the way I do it. For this task, I'm doing something similar. In the previous episode, I got to a green Fit cell, creating two Presenter objects, two Mock Views, and two abstract View classes along the way. Now I'm going to flesh out the meaning of statements like "choosing Betsy with owner Rankin". The interaction designer's wireframe sketch shows that done through a list. But when I make that list, a host of questions pop up, like "When you add a new case, is it automatically selected in the list?" Talking about properties of particular UI elements seemed like a job for unit-size tests. It was also painfully obvious that I needed more UI. There's a place in the wireframe to select a case, but no place to add one. For the sake of expediency, I hung that UI off the side of what I was given, well aware that I'll probably change it later. Because I've never programmed to this Macintosh UI framework, I made a simplified UI for my first leap into the unknown. The end result looked like the picture on the right. I now worked through the Fit workflow, adding Macintosh detail to each step. At first, I began each step with a spike so I could find out what kind of messages flow from the UI to my Views. (Yes, I could have read all the documentation - think of the spike as guiding me through the reference manuals.) For my UI's Views to be really thin, there has to be a one-to-one correspondence between messages from the UI and the messages flowing to the Presenter. For example, here's what happens when a user presses TAB or ENTER in a text field: public void animalNameEntered(Object sender) { /* IBAction */ myPresenter().animalNameEntered(animalNameField.stringValue()); } As I learned more, I got bolder about unit-testing behavior into existence with a Mock View and only then making that behavior work with the real View. Sometimes I regretted that, discovering that my assumption about what the UI must do was wrong. It was a little harder than usual to muster enthusiasm for the junit tests. For example, when I wanted to make the Presenter send a "highlight row 0" message to the table, I wrote a test that had the Presenter send that message to the Mock View and then make checks like these: assertTrue(inpatientView().wasARowHighlighted()); assertEquals(0, inpatientView().getHighlightedRowIndex()); ... and then I had to go make the Mock View remember the message was called so that my test could ask about it. Seems like a lot of work to drive one silly message from the View to the UI. (Perhaps a mock-builder would help?) This bothers me because I have a principle that if programmers are finding testing annoying, that's a problem to fix. One way to fix it is to make the work provide value to new people. Consider: I was working on the code that handles entering a patient name, entering an owner name, and clicking the "add case" button. At that point in implementation, it was natural for me to ask an imaginary UI designer about error cases. What should happen if you hit the button before typing in an animal name? What happens if you hit the button twice? Etc. "We" decided not to code up some of the error cases right away. But some we did. Suppose my designer had said that pressing the Add Case button without both fields filled in should produce an error. Here's a table that might record our conversation:
The reason I like this is the reason I like being systematic. When you're constructing unit tests one at a time, it's easy to overlook a situation like both fields being empty. Suppose I'd only thought of the first three tests, in that order. My checking code would almost certainly first check for an empty owner name. So the behavior for that situation would also be the behavior for the situation where neither name was given. That's actually bad. Because the owner entry field follows the animal entry field, there's a fair chance the user would respond to the error by entering the owner name, hitting Enter, then getting annoyed by another error message. It's better for her to be directed to the first field in the tab order. That particular example isn't a big deal, but sometimes the case you overlook is a very big deal indeed. By making a quick table, I not only increase the chance of thinking of all important cases, I also reduce the chance of overlooking a kind of result (like what gets highlighted). Now, as it happens, my imaginary designer chose a different way to solve the problem. The Add Case button should be greyed out until both names are available. That behavior was driven by unit tests like these: public void testCreationRequiresBothNames() { assertEquals(disallowed, inpatientView().getAddCaseCommands()); inpatientView().animalNameEntered("animal data"); assertEquals(disallowed, inpatientView().getAddCaseCommands()); inpatientView().ownerNameEntered("owner data"); assertEquals(nowAllowed, inpatientView().getAddCaseCommands()); } public void testEnteringCaseErasesPreviousNames() { inpatientView().ownerNameEntered("owner"); inpatientView().animalNameEntered("animal"); inpatientView().addCasePressed(); assertEquals(disallowedAgain, inpatientView().getAddCaseCommands()); assertTrue(inpatientPresenter().hasNoAnimalName()); assertTrue(inpatientPresenter().hasNoOwnerName()); assertTrue(inpatientView().wasTheAnimalFieldNameEmptied()); assertTrue(inpatientView().wasTheOwnerNameFieldEmptied()); } So. I didn't think of making that table; I wrote jUnit tests instead. I'm tempted to keep coding. But the point of this exercise is not to produce a program, it's to learn stuff. So what I'll do is back up, write some Fit tests, and ask myself questions like these:
(Note that throughout I'm assuming the hypothetical Good Fit Editor, one that makes creating and modifying tables as easy as creating and modifying a small RTF document with your editor of choice. We need a Good Fit Editor!) As usual, you can find the current code in zip file. There are two separate projects - the core code and the Cocoa interface. I'd rather they were all in one directory structure, but I couldn't get IDEA and Xcode/IB to play nice together. |
|