I'm not a particularly good tennis player. Mostly from not having played in years, but even then, I suffered because I can't hit a decent backhand to save my life. I have a big serve and a decent control with my forehand, but all you've got to do to beat me is put the ball where I have to attempt a backhand. All of the best competitive tennis players can will choose either the backhand or forehand depending on where on the court they play the ball.
Test Driven Developers need to be the same way in regards to choosing a state based style or interaction based style on every single unit test. I've been disappointed with the recent conversations I've seen lately on the subject. Many people are proudly describing themselves as Classicist or Mockist TDD'ers because they favor either state or interaction based unit tests and see the other style as something to avoid. Specifically, I'm agitated at the people who are trumpeting themselves as "Classicists" because I feel like they're doing real harm to the state of TDD practice. I've never met a self-described mockist and I'm tempted to say that the very term is a strawman. On the other hand, self-described Classicists may be just like me on the tennis court -- they're taking bad shots and bad angles just so they can hit a forehand when a backhand is more appropriate.
Over Specified Test
Many people are concerned about the "over specified test" anti-pattern and rightly blame mocks as a primary source. So mock objects are evil right? Survey says: bzzzt. An over specified interaction test is generally caused by a fine-grained, chatty API or a lack of encapsulation between a class and it's dependencies. In other words, you may be staring at a code smell. I ran into this lately at work. I had a Supervising Controller that was issuing a lot of fine-grained commands to its View. Make the addresses editable, make the address read only, show Canadian address fields, make the stored address dropdown shown for certain customers, and hide the stored address dropdown at other times. The code became nasty and the unit tests got ugly with runaway expectations. I refactored the Supervising Controller to a bit of a state machine that could correctly create a "ScreenState" object at any point and rigorously tested that logic. I then changed the interaction between View and Presenter to just ensuring that the Presenter was correctly passing a new ScreenState to the View upon certain View events.
Some points about over specified interaction tests:
- Do not try to mock chatty interfaces. Favor coarse grained API's that hide more details of the internals of a class's dependencies. I wrote a post a couple years back called Best and Worst Practices for Mock Objects. I read over it this morning and I think it still holds up.
- When you spot an over specified test brewing in your code, treat it as a code smell and reevaluate your class structure. Remember that TDD is a DESIGN PROCESS. If the unit test is going badly, your first assumption should be that the design needs refinement. Listen to what the unit test is telling you about your code.
- Be aggressive with dynamic and partial mocks to write smaller, more focused interaction based tests
- You don't have to always use ReplayAll() and VerifyAll(). Sometimes you might want to call Verify on only one mock object to create a smaller, more focused unit test
State Based Testing isn't all that and a bag of chips
I generally tell people that State Based Testing is easier than Interaction Based Testing on the whole -- except when it's not. Let's consider this state based test:
[Test]
public void The_presenter_saves_the_whatsit_if_the_whatsit_is_valid()
{
Whatsit model = ObjectMother.ValidWhatsit();
WhatsitPresenter presenter = new WhatsitPresenter(new StubWhatsitView(), model, new StubWhatsitRepository());
bool returnValue = presenter.Save();
// Look ma! I should have saved the Whatsit in this scenario
Assert.IsTrue(returnValue);
}
[Test]
public void The_presenter_does_NOT_save_the_whatsit_if_the_whatsit_is_InValid()
{
Whatsit model = ObjectMother.InvalidWhatsit();
WhatsitPresenter presenter = new WhatsitPresenter(new StubWhatsitView(), model, new StubWhatsitRepository());
bool returnValue = presenter.Save();
Assert.IsFalse(returnValue);
}
When the Save() method of my WhatsitPresenter is called the WhatsitPresenter should perform a validation of its Whatsit member and either use the WhatsitRepository to save the Whatsit, or have the WhatsitView display the validation errors. That's the intent of the unit tests above, but how much of that intent is really coming through in these unit tests? Very little right? We're doing a nice simple state based test to check that the Save() method correctly returns a boolean value saying that it did or did not save the Whatsit. That's nice, but the real intent behind the test is that the Whatsit was really saved. This is an example of a bad unit test. We're not even testing the real functionality, we're just testing a side effect of the code by checking the return value. What we really need to do is to verify that our WhatsitPresenter did or did not send a Save() message to the WhatsitRepository, i.e. we should use some sort of mock object here.
I use a lot of contrived examples in my blog, but this example was adapted from another blogger. Sometimes, favoring a state based testing philosophy leads you to useless tests that test through side effects. Other times, a state based test will cause you to relax encapsulation in a harmful way to make the state based assertions where an interaction based test would maintain encapsulation. When you play a ball on the backhand side of the court, you better use your backhand.
Bottom Up versus Top Down
Do you start from the "top" and code that against mock objects for the lower level concerns, or do you write the lower level pieces first, then assemble them together to create the aggregate structure? Which is best? Well, it depends. Here's an easy rule of thumb. Start with whatever you do know how to do.
If you know exactly how one or more steps of a complex algorithm should work, build those steps first in isolation. Building out those steps will often suggest the structure of the coordinating code. That's bottom up development.
At other times, you'll know the general workflow of the code, but not necessarily know how some of the lower level tasks will be performed. In this scenario I write the controller type unit tests first and just drop off mock objects as a placeholder for concrete classes to be added later. This is frequently a great way to work with any variant of the Model View Presenter pattern. Building the overarching workflow will help define the API to the underlying steps in the algorithm and often give you some insight into how those tasks should be implemented.
I don't see the usage of acceptance tests to have any impact whatsoever on my choice of bottom up or top down. Either way, I'm going to break down the feature into a finite number of coding tasks and write fine grained unit tests for those tasks until I feel like it's appropriate to start running the coarse grained acceptance test.
The beauty of TDD as a design process is the ability it grants you to work on one issue at a time without leaving yourself in the hole to create the other pieces.
Summary
You might have a mighty forehand with your state based tests, but there are going to be times when it's better to swing a backhand stroke and use an interaction based test with a mock object. My advice is to focus on the goal of each unit test and make sure the unit test is mostly concerned with that goal. If the goal of a unit test is to ensure that a change of state or a return value is correct, it's a state based test. If the goal of a test is to ensure that a class is passing the correct messages to other classes during its internal functioning, you should be writing an interaction test. Don't pick sides on the Classicist versus Mockist argument. It's a false dichotomy and a harmful line of thought. You aren't going into your toolbox at home and throwing out all the close-ended wrenches because you only want to use open-ended wrenches are you? Same thing applies to mocks.