2010-08-31

Mocking built-in functions in PHP

I just released a library for mocking built-in PHP functions for testing purposes. It can be found at http://github.com/jadell/PHPBuiltinMock.

The README file is pretty detailed about how to use the library, so I wanted to write a bit about why to use it.

PHPBuiltinMock is used to control the output of PHP functions who's usual output is indeterminate. Built-in mocking allows you to treat these indeterminate calls like external dependencies, and mock them.

A classic example is rand(). It is very difficult to write tests against code that uses rand() because the output of the code changes with each run.

class MyClass
{
    public function multiplyWillyNilly($iWilly)
    {
        $iNilly = rand();
        return $iWilly * $iNilly;
    }
}

class MyClassTest extends PHPUnit_Framework_TestCase
{
    public function testMultiply_HopeWeGetA21()
    {
        $iExpected = 21;

        $oClass = new MyClass();
        $iResult = $oClass->multiplyWillyNilly(3);

        // This will work sometimes, but is incredibly brittle!
        self::assertEquals($iExpected, $iResult);
    }
}

The usual way to avoid this problem is to wrap the indeterminate call in another method and mock the call to that method.

class MyClass
{
    public function multiplyWillyNilly($iWilly)
    {
        $iNilly = $this->myRand();
        return $iWilly * $iNilly;
    }

    protected function myRand()
    {
        return rand();
    }
}

class MyClassTest extends PHPUnit_Framework_TestCase
{
    public function testMultiply_Return21()
    {
        $iExpected = 21;

        $oClass = $this->getMock('MyClass', array('myRand'));
        $oClass->expects($this->once())
            ->method('myRand')
            ->will($this->returnValue(7));

       $iResult = $oClass->multiplyWillyNilly(3);
        self::assertEquals($iExpected, $iResult);
    }
}

This does accomplish the trick, but at the expense of code complexity. If all the method does is wrap and return a single function call, it probably doesn't need to be it's own method.

Let's look at our purpose for wanting to mock the rand() call. We don't actually care what the result of rand() is, just that the method calls it. We can keep the code simple and still accomplish this goal by using built-in mocking.

class MyClass
{
    public function multiplyWillyNilly($iWilly)
    {
        $iNilly = rand();
        return $iWilly * $iNilly;
    }
}

class MyClassTest extends PHPUnit_Framework_TestCase
{
    public function teardown()
    {
        BuiltinMock::restore('rand');
    }

    public function testMultiply_IThinkWeWillGetA21()
    {
        $iExpected = 21;

        BuiltinMock::override('rand', new BuiltinMock_Returner_SetValue(7));

        $oClass = new MyClass();
        $iResult = $oClass->multiplyWillyNilly(3);

        self::assertEquals($iExpected, $iResult);
    }
}

We didn't have to modify the code being tested, and we have still accomplished our goal in testing.

Once you get the hang of mocking built-in functions, many uses present themselves. We use built-in mocking to test various scenarios using curl_exec() without requiring an external webserver, and file_get_contents() and file_put_contents() without needing real files or places to write them.

One of the most common uses in our tests is "pinning time." We can run entire test suites pretending that it is the end of February in a leap year, or crossing DST boundaries, or any number of other time-sensitive problem scenarios. Tests would sometimes fail not because the code was incorrect, but because the test machine was under heavy load, which slowed the test down, and expected timestamps would be off by one or more seconds. That problem has virtually disappeared thanks to built-in mocking of the time() function.

1 comment:

  1. Check php-mock which is a mock library for built-ins.

    ReplyDelete