Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unit test that exception is thrown on nth call to function

Tags:

php

phpunit

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.

like image 935
Shane Avatar asked Oct 13 '12 16:10

Shane


4 Answers

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();
    }
}
like image 136
PeeHaa Avatar answered Oct 14 '22 22:10

PeeHaa


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.

like image 33
user1708452 Avatar answered Oct 11 '22 05:10

user1708452


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.

like image 4
rdlowrey Avatar answered Oct 14 '22 22:10

rdlowrey


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');
            }
        }
    }
}
like image 1
Shane Avatar answered Oct 14 '22 20:10

Shane