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:
Here's a simple example of a test with multiple assertions:
function testSomethingThatReturnsAnArray() {This could easily be rewritten to multiple assertions:
$value = somethingThatReturnsAnArray();
$this->assertEquals('Foo', $value[0]);
$this->assertEquals('Bar', $value[1]);
$this->assertEquals('Mitz', $value[2]);
}
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:
Better yet, you can test each piece on its own:
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:
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() {This allows you to change the peel calculation without touching the test for size, or the size calculation without touching the peel calculation.
$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']);
}
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:
/**Each split gives you more information on a failure and makes debugging test failure easier as well as reducing the changes required for refactoring.
* @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']);
}