I recall from back when the Earth was cooling and I was spending a of time in Seattle (read: "living there") that there was a lot of advice flying around about the forces surrounding designing individual tests.
A particular point of interest was when to factor stuff out into a setup method. One camp said "never factor something into a setup method until it's needed by more than one test." Another camp said "factor stuff out into a setup method as soon as you reasonably can."
At the time, I didn't really care so I just went along with my friends. Now I think I do care. I've noticed that factoring certain things out into a set up method improves the readability of a test, even when there is only one.
That, to me, settles the "when" question by obviating it and uncovering a much more interesting question: what should you factor out into a set-up method?
I've noticed that, for me, there are two kinds of set-up needed: context stuff and test-specific stuff. The context stuff is obvious. The test-specific stuff is interesting.
For a human, there is no information in a context-setting line of code. It is still informative to a compiler, which is why you need it, but a person trying to understand an API would easily fill in the blanks if he never saw that line of code. The only real effect such a line of code has on a human-reader is to make a test less-readable by adding noise. An example of context-setting set-up is instantiating the class being tested.
By contrast, the test-specific line of code is very interesting to a human. It explains what is happening or why it is happening. Examples of test-specific code would be invoking a method on the class under test or asserting on the results of such an invocation.
Let's look at two examples.
Here's a test with all the set-up in the test method to prevent a preemptive refactor:
[Test]
public void UnderOrdinaryCircumstancesPassesThroughToCore()
{
var core = new Mock<PathFinder>();
var specialEdge = new Mock<PathFinderEdge>();
var specialEdgeFinder =
SpecialFirstEdgePathFinder.GetInstance(core.Object, specialEdge.Object);
var origin = Design.Element.GetInstance(null, null, null);
var keyPath = new[] { Any.Symbol, Any.Symbol };
var elementPath = new[] {
Design.Element.GetInstance(null, null, null),
Design.Element.GetInstance(null, null, null), };
core.Setup(c => c.Navigate(origin, keyPath)).Returns(elementPath);
var actual = specialEdgeFinder.Navigate(origin, keyPath).ToArray();
Assert.That(actual, Is.EqualTo(elementPath));
}
Here's a test with all the set-up in the test method to prevent a preemptive refactor:
[Test]
public void UnderOrdinaryCircumstancesPassesThroughToCore()
{
var keyPath = new[] { Any.Symbol, Any.Symbol };
var elementPath = new[] {
Design.Element.GetInstance(null, null, null),
Design.Element.GetInstance(null, null, null), };
core.Setup(c => c.Navigate(origin, keyPath)).Returns(elementPath);
var actual = specialEdgeFinder.Navigate(origin, keyPath).ToArray();
Assert.That(actual, Is.EqualTo(elementPath));
}
The two tests specify the exact same thing. The only difference, in my mind, is how they read. To me, the latter is a lot more clear than the former.
The process that works for me is as follows:
- Write the test with no set-up to discover what the test should be
- Make the test pass
- In the refactoring phase, move all the "clutter" to a context-setting setup method
I'm open to other arguments but this is working for me now.