Thinking back over my post from a year ago
I find that the real reason for most bad unit tests is that people are
trying too hard, typically for one of the following reasons:
- Some folks have drunk the "don't repeat yourself" KoolAid: I agree that not
repeating code is a virtue in most cases, but unit test code is an
exception: cleverness in a test both obscures the intent of the test
and makes a subsequent failure massively harder to diagnose.
- Others want to avoid writing both tests and documentation: they try to write
test cases (almost invariably as "doctests") which do the work of real tests,
while at the same time trying to make "readable" docs.
Most of the issues involved with the first motive are satisfactorily addressed
in the earlier post: refusing to share code between test modules makes most
tempations to cleverness go away. Where the temptation remains, the cure is
to look at an individual test and ask the following questions:
- Is the intent of the test clearly explained by the name of the testcase?
- Does the test follow the "canonical" form for a unit test? I.e., does it:
- set up the preconditions for the method / function being tested.
- call the method / function exactly one time, passing in the values
established in the first step.
- make assertions about the return value, and / or any side effects.
- do absolutely nothing else.
Fixing tests which fail along the "don't repeat yourself" axis is usually
straightforward:
- Replace any "generic" setup code with per-test-case code. The classic
case here is code in the
setUp
method which stores values on the self
of the test case class: such code is always capable of refactoring
to use helper methods which return the appropriately-configured test
objects on a per-test basis.
- If the method / funciton under test is called more than once, clone
(and rename appropriately) the test case method, removing any redundant
setup / assertions, until each test case calls it exactly once.
Rewriting tests to conform to this pattern has a number of benefits:
- Each individual test case specifies exactly one code path through the
method / function being tested, which means that achieving "100% coverage"
means you really did test it all.
- The set of test cases for the method / function being tested define the
contract very clearly: any ambiguity can be solved by adding one or more
additional tests.
- Any test which fails is going to be easier to diagnose, because the
combination of its name, its preconditions, and its expected results are
going to be clearly focused.
Copyright © 1996-2008, Tres Seaver, Palladion Software