2011-12-19

One assertion per test


There's a right way and a wrong way to write unit tests. I've seen lots of wrong ways (like unit tests with no assertions) and differing shades of rightness. One thing that isn't really "wrong" but is not as right is writing your test cases with multiple assertions per test case.

Here's a simple example of a test with multiple assertions:
function testSomethingThatReturnsAnArray() {
    $value = somethingThatReturnsAnArray();
    $this->assertEquals('Foo', $value[0]);
    $this->assertEquals('Bar', $value[1]);
    $this->assertEquals('Mitz', $value[2]);
}
This could easily be rewritten to multiple assertions:
function testSomethingThatReturnsAnArrayFirstElement() {
    $value = somethingThatReturnsAnArray();
    $this->assertEquals('Foo', $value[0]);
}

function testSomethingThatReturnsAnArraySecondElement() {
    $value = somethingThatReturnsAnArray();
    $this->assertEquals('Bar', $value[1]);
}

function testSomethingThatReturnsAnArrayThirdElement() {
    $value = somethingThatReturnsAnArray();
    $this->assertEquals('Mitz', $value[2]);
}
What does this gain? You can immediately see whether somethingThatReturnsAnArray() is broken for all of the tests or just the first. With the first example, if $value[0] is not 'Foo', the test will immediately fail and you won't even test whether the next two elements are correct or not.


But the method I'm testing is slow!


If somethingThatReturnsAnArray() is so slow that you don't want to run it three times, you've got two options:

  • Make the method faster.
  • Only run it once per test suite run.
Obviously making the method faster would benefit your code the most overall, but it isn't always possible. But you can always just run it once, either by saving its output as a member variable of the test suite or making the tests depend on each other.

Saving the output is easy:
$result = null;
public function testSomethingSlowThatReturnsComplexResult() {
    $this->result = somethingThatReturnsComplexResult();
    $this->assertEquals('Foo', $this->result[0]);
}

public function testSomethingSlowThatReturnsComplexResultSecond() {
    $this->assertEquals('Bar', $this->result[1]);
}
But there is a problem. Tests shouldn't require a certain order unless it is explicitly set. If you move the second test method above the first it will start failing. You could have a helper method that only executes the function once:
$result = null;
private function runSomethingSlow() {
    if (!$this->result) {
        $this->result = somethingThatReturnsComplexResult();
    }
    return $this->result;
}

public function testSomethingSlowThatReturnsComplexResult() {
    $result = runSomethingSlow();
    $this->assertEquals('Foo', $result[0]);
}

public function testSomethingSlowThatReturnsComplexResultSecond() {
    $result = runSomethingSlow();
    $this->assertEquals('Bar', $result[1]);
}

Now the slow function only is executed a single time, and the test methods don't have an unnamed dependency on any other tests.


But my method is really complicated!

If you have a very complicated method that has lots of dependencies, you may be tempted to test actions on all of the dependencies at the same time as well as the return value so you don't have to have all of the boilerplate code repeated. I've heard some otherwise very talented coders make this argument, which always surprises me. Test code is not really different from production code in that if you find that you're repeating code several times, it should be factored out into a method.

Let's try testing everything about this method:
function compareAppleToOrange(Apple $apple, Orange $orange) {
    return array(
        'sizeDifference' => $apple->getSize()
                - $orange->getSize(),
        'peelDifference' => $apple->getPeelWidth()
                - $orange->getPeelWidth(),
    );
}
You could test it all in one big test:
function testCompareAppleToOrange() {
    $apple = $this->getMock('Apple',
        array('getSize', 'getPeelWidth'));

    $apple->expects($this->once())
        ->method('getSize')
        ->will($this->returnValue(4));
    $apple->expects($this->once())
        ->method('getPeelWidth')
        ->will($this->returnValue(2));

    $orange = $this->getMock('Orange',
        array('getSize', 'getPeelWidth'));

    $orange->expects($this->once())
        ->method('getSize')
        ->will($this->returnValue(3));
    $orange->expects($this->once())
        ->method('getPeelWidth')
        ->will($this->returnValue(3));

    $expected = array(
        'sizeDifference' => 1,
        'peelDifference' => -1,
    );
    $result = compareAppleToOrange($apple, $orange);
    $this->assertEquals($expected, $result);
}

Better yet, you can test each piece on its own:
function testCompareAppleToOrangeSize() {
    $apple = $this->getMock('Apple',
        array('getSize', 'getPeelWidth'));

    $apple->expects($this->once())
        ->method('getSize')
        ->will($this->returnValue(4));
    $apple->expects($this->any())
        ->method('getPeelWidth');

    $orange = $this->getMock('Orange',
        array('getSize', 'getPeelWidth'));

    $orange->expects($this->once())
        ->method('getSize')
        ->will($this->returnValue(3));
    $orange->expects($this->any())
        ->method('getPeelWidth');

    $result = compareAppleToOrange($apple, $orange);
    $this->assertEquals(1, $result['sizeDifference']);
}

function testCompareAppleToOrangePeel() {
    $apple = $this->getMock('Apple',
        array('getSize', 'getPeelWidth'));

    $apple->expects($this->any())
        ->method('getSize');
    $apple->expects($this->once())
        ->method('getPeelWidth')
        ->will($this->returnValue(2));

    $orange = $this->getMock('Orange',
        array('getSize', 'getPeelWidth'));

    $orange->expects($this->once())
        ->method('getSize')
        ->will($this->returnValue(3));
    $orange->expects($this->any())
        ->method('getPeelWidth');

    $result = compareAppleToOrange($apple, $orange);
    $this->assertEquals(-1, $result['peelDifference']);
}
This allows you to change the peel calculation without touching the test for size, or the size calculation without touching the peel calculation.

If you wanted to really split the test up, you can go even further since the mocks  provide some assertions of their own with the expects() calls. We'll split up the sizeDifference tests:

/**
 * @return array Array from apples to orange comparison.
 */
function testCompareAppleToOrangeSizeCalls() {
    $apple = $this->getMock('Apple',
        array('getSize', 'getPeelWidth'));
    $apple->expects($this->once())
        ->method('getSize')
        ->will($this->returnValue(4));
    $apple->expects($this->any())
        ->method('getPeelWidth');

    $orange = $this->getMock('Orange',
        array('getSize', 'getPeelWidth'));
    $orange->expects($this->once())
        ->method('getSize')
        ->will($this->returnValue(3));
    $orange->expects($this->any())
        ->method('getPeelWidth');

    return compareAppleToOrange($apple, $orange);
}

/**
 * @depends testCompareAppleToOrangeSizeCalls
 * @param array $result Result of comparison.
 */
function testCompareAppleToOrangeSize($result) {
    $this->assertEquals(1, $result['sizeDifference']);
}
Each split gives you more information on a failure and makes debugging test failure easier as well as reducing the changes required for refactoring.

1 comment:

  1. I've been pro-TDD since it came out way back when Kent Beck was a household name. However, I just can't find any reasonable justification for one assertion per test. The volume of typing required to make it happen just doesn't justify the purported benefits, which I've personally never experienced when I did do it that way.

    I'll try to address things in the order presented.

    function testSomethingThatReturnsAnArray() {
    $value = somethingThatReturnsAnArray();
    $this->assertEquals('Foo', $value[0]);
    $this->assertEquals('Bar', $value[1]);
    $this->assertEquals('Mitz', $value[2]);
    }

    This is, frankly, absolutely correct, in the correct context. You assert breaking the assertions out into individual components makes it possible to see where some values are correct, and others fail, completely independently. If this is a virtue for you, you're treating the array as a structure, and you should be using a structure, not an array.

    However, if you're returning a true array of values, then a single discrepancy in that array implies that the entirety of the array is wrong. I care not one wit if elements 0 and 2 are correct if element 1 is wrong. An array, mathematically speaking, is a single object, and if any one element is wrong, the whole array is wrong by definition. In this context, asserting every element in a single function not only makes perfect sense, it's actually more correct.

    Exercising slow functions best occurs through separating your test =classes=, not the test methods. If you find yourself memoizing heavy-weight objects, your code either is not factored well enough for effective unit testing (it's becoming more of an integration/smoke test), or your test classes are not properly factored in the first place.

    While I agree with your concerns over combining mock-based tests with value assertions, I find your stated rationale specious:

    "This allows you to change the peel calculation without touching the test for size, or the size calculation without touching the peel calculation."

    In the code listed above, changing either calculation requires changing some flavor of assertion, whether it is in the mock configuration or the value to be asserted. Whether the lines of code changed appears inside one definition or another isn't relevant.

    I think a more correct explanation would be that the tests are more self-documenting that way.

    Speaking of which, the names used to write the tests above are, to be blunt, atrocious. They don't tell me anything about what the test itself is attempting to assert (and, yes, sometimes, tests are not obvious, as I've so painfully learned working with java-based testing).

    For example, instead of "testCompareAppleToOrangeSizeCalls" contained in some genericly named class, I would instead place a method named "testShouldDetermineSize" inside a class explicitly named "WhileTryingToCompareApplesAndOranges". This is basic DRY applied to naming, and it reads much more fluently when attempting to maintain tests.

    ReplyDelete