Wednesday, 13 July 2011

TDD tests aren’t unit tests; don’t be tempted to peek under the hood

Realise that to simply focus-test methods in isolation is wrong. The problem is, you probably don’t realise you’re doing it.

First of all, the assumption of isolation is a fallacy – you can’t just call a method; typically you have to call a constructor as well, at least. Kevlin Henney expresses this well.

Second, it encourages you to add backdoors to implementation so that you can bring an instance up to testable state:

public class CuriousList
{
    private IList<string> _underlying;

    public CuriousList(IList<string> underlying)
    {
        // Here the natural relationship between CuriousList and _underlying
        // is of composition, not aggregation. It's an implementation detail.
        // There's no need for me to be injecting it, and it exposes internal
        // state ...
        _underlying = underlying;
    }

    public void Add(string newString)
    {
        _underlying.Add(newString);
    }

    public void RuinListElements()
    {
        for(int i = _underlying.Count - 1; i >= 0; i--)
        {
            _underlying[i] = i.ToString();
        }
    }
}

When it comes to testing RuinListElements(), I might be happy to inject the underlying collection just so I could add some reasonable state to act on. Like this:

    
[TestFixture]
public class CuriousListTests
{
    [Test]
    public void CuriousList_AfterAnElementIsAdded_CanRuinIt()
    {
        var u = new List<string>() { "Hello" };

        var curiousList = new CuriousList(u);

        curiousList.RuinListElements();

        Assert.AreEqual("0", u[0]);
    }
}

Tempting as it is for tests, it leaves a big backdoor into what your object considers private state. Even if you add a helpful comment saying “this injection ctor to be used for testing only” it’s asking for trouble; you might mislead yourself by using it to reconstruct state that can’t be arrived at by your public methods. Chris Oldwood talks about something similar.

Avoid using injection for compositional relationships; only use it for aggregation. 

Instead, use public methods that bring the object neatly into the state you need in order to test what you want to test. Don’t be concerned that those same public methods are being tested in other tests. You need all your tests to be passing in any case; you don’t gain much by uncoupling individual tests.

Afterwards, use public methods to test the object’s state. Don’t peek under the hood to see what happened. Naturally you can query the state of any mocks you used; that’s an aggregational relationship and what they’re designed for.

Remember that you might well end up with tests that map to single functions – especially for functions that don’t change state, or move the object from no-state to some-state. The tests will appear to be the same as those produced by simply focus-testing method, but the intent is quite different.

All this frees you up to think about testing behaviour, not testing methods. A specific behaviour of your class will often, if not always, be an aggregate of method calls.

No comments: