In this Zero-Friction TDD post, I'd like to take a detour around the concept of tests as Executable Specification.

An important aspect of test maintainability is readability. Tests should act both as Executable Specification as well as documentation, which puts a lot of responsibility on the test.

One facet of test readability is to make the relationship between the Fixture, the SUT and the verification as easy to understand as possible. In other words, it should be clear to the Test Reader what is being asserted, and why.

Consider a test like this one:

[TestMethod]
public void InvertWillReverseText_Naïve()
{
    // Fixture setup
    MyClass sut = new MyClass();
    // Exercise system
    string result = sut.Invert("ploeh");
    // Verify outcome
    Assert.AreEqual<string>("heolp", result, "DoWork");
    // Teardown
}

Since this test is so simple, I expect that you can easily figure out that it implies that the Invert method should simply reverse its input argument, but one of the reasons this seems to be evident is because of the proximity of the two strings, as well as the test's name.

In a test of a more complex API, this may not be quite as evident.

[TestMethod]
public void DoItWillReturnCorrectResult_Naïve()
{
    // Fixture setup
    MyClass sut = new MyClass();
    // Exercise system
    int result = sut.DoIt("ploeh");
    // Verify outcome
    Assert.AreEqual<int>(42, result, "DoIt");
    // Teardown
}

In this test, there's no apparent relationship between the input (ploeh) and the output (42). Whatever the algorithm is behind the DoIt method, it's completely opaque to the Test Reader, and the test fails in its role as specification and documentation.

Returning to the first example, it would be better if the relationship between input and output was explicitly described:

[TestMethod]
public void InvertWillReverseText()
{
    // Fixture setup
    string anonymousText = "ploeh";
    string expectedResult =
        new string(anonymousText.Reverse().ToArray());
    MyClass sut = new MyClass();
    // Exercise system
    string result = sut.Invert(anonymousText);
    // Verify outcome
    Assert.AreEqual<string>(expectedResult, result,
        "DoWork");
    // Teardown
}

In this case, the input and expected outcome are clearly related, and we call the expectedResult variable a Derived Value, since we explicitly derive the expected result from the input.

Note that I'm not asking you to re-implement the whole algorithm in the test, but only to establish a relationship. One of the main rules of thumb of unit testing is that a test should never contain conditional branches, so there must be at least one test case per path though the SUT.

In the example, the Invert method actually looks like this:

public string Invert(string message)
{
    double d;
    if (double.TryParse(message, out d))
    {
        return (1d / d).ToString();
    }
 
    return new string(message.Reverse().ToArray());
}

Note that the above test only reproduces that part of the algorithm that corresponds to the Equivalence Class defined by the input, whereas the branch that is triggered by a number string can be tested by another test case that doesn't specify string reversion.

[TestMethod]
public void InvertWillInvertNumber()
{
    // Fixture setup
    double anonymousNumber = 10;
    string numberText = anonymousNumber.ToString();
    string expectedResult = 
        (1d / anonymousNumber).ToString();
    MyClass sut = new MyClass();
    // Exercise system
    string result = sut.Invert(numberText);
    // Verify outcome
    Assert.AreEqual<string>(expectedResult, result,
        "DoWork");
    // Teardown
}

In this way, we can break down the test cases to individual Executable Specifications that define the expected behavior for each Equivalence Class.

While such tests more clearly provide both specification and documentation, it requires discipline to write tests in this way. Particularly when the algorithm is so simple as is the case here, it's very tempting to just hard-code the values directly into the assertion.

In a future post, I'll explain how we can force ourselves to do the right thing per default.



Wish to comment?

You can add a comment to this post by sending me a pull request. Alternatively, you can discuss this post on Twitter or somewhere else with a permalink. Ping me with the link, and I may respond.

Published

Tuesday, 03 March 2009 20:01:29 UTC

Tags



"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!
Published: Tuesday, 03 March 2009 20:01:29 UTC