Say you have a method which ultimately boils down to
class Pager
{
private $i;
public function next()
{
if ($this->i >= 3) {
throw new OutOfBoundsException();
}
$this->i++;
}
}
How would you unit test this class. I.e. test whether the exception gets thrown on the third call of next()
using PHPUnit? I've added my attempt as an answer, but I'm not sure whether this really is the way to go.
What about testing for null
on the first two calls and also test for the exception being thrown like follows:
class PagerTest
{
public function setUp()
{
$this->pager = new Pager();
}
public function testTooManyNextCalls()
{
$this->assertNull($this->pager->next());
$this->assertNull($this->pager->next());
$this->assertNull($this->pager->next());
$this->setExpectedException('OutOfBoundsException');
$this->pager->next();
}
}
You could use something like:
class PagerTest extends PHPUnit_Framework_TestCase {
/**
* @expectedException OutOfBoundsException
*/
public function testTooManyNextCalls() {
$this->pager = new Pager();
$this->pager->next();
$this->pager->next();
$this->pager->next();
$this->assertTrue(false);
}
}
If an exception is thrown in the 3rd method call, the always-failing assert-statement should never be reached and the test should pass. on the other hand if no exception gets thrown, the test will fail.
It's very important when unit-testing to avoid testing implementation details. Instead, you want to limit yourself to testing only the public interface of your code. Why? Because implementation details change often, but your API should change very rarely. Testing implementation details means that you'll constantly have to rewrite your tests as those implementations change, and you don't want to be stuck doing this.
So what does this mean for the OP's code? Let's look at the public Pager::next
method. Code that consumes the Pager
class API doesn't care how Pager::next
determines if an exception should be thrown. It only cares that Pager::next
actually throws an exception if something is wrong.
We don't want to test how the method arrives at it's decision to throw an OutOfBoundsException
-- this is an implementation detail. We only want to test that it does so when appropriate.
So to test this scenario we simulate a situation in which the Pager::next
will throw. To accomplish this we simply implement what's called a "test seam." ...
<?php
class Pager
{
protected $i;
public function next()
{
if ($this->isValid()) {
$this->i++;
} else {
throw new OutOfBoundsException();
}
}
protected function isValid() {
return $this->i < 3;
}
}
In the above code, the protected Pager::isValid
method is our test seam. It exposes a seam in our code (hence the name) that we can latch onto for testing purposes. Using our new test seam and PHPUnit's mocking API, testing that Pager::next
throws an exception for invalid values of $i
is trivial:
class PagerTest extends PHPUnit_Framework_TestCase
{
/**
* @covers Pager::next
* @expectedException OutOfBoundsException
*/
public function testNextThrowsExceptionOnInvalidIncrementValue() {
$pagerMock = $this->getMock('Pager', array('isValid'));
$pagerMock->expects($this->once())
->method('isValid')
->will($this->returnValue(false));
$pagerMock->next();
}
}
Notice how this test specifically doesn't care how the implementation method Pager::isValid
determines that the current increment is invalid. The test simply mocks the method to return false
when it's invoked so that we can test that our public Pager::next
method throws an exception when it's supposed to do so.
The PHPUnit mocking API is fully covered in Test Doubles section of the PHPUnit manual. The API isn't the most intuitive thing in the history of the world, but with some repeated use it generally makes sense.
Here's what I have at the moment, but I was wondering if there were any better ways of doing this.
class PagerTest
{
public function setUp()
{
$this->pager = new Pager();
}
public function testTooManyNextCalls()
{
for ($i = 0; $i < 10; $i++) {
try {
$this->pager->next();
} catch(OutOfBoundsException $e) {
if ($i == 3) {
return;
} else {
$this->fail('OutOfBoundsException was thrown unexpectedly, on iteration ' . $i);
}
}
if ($i > 3) {
$this->fail('OutOfBoundsException was not thrown when expected');
}
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With