Fork me on GitHub

PHPTestDummy by tcz

Alternative PHP mock object.

Tutorial

Constructing

First of all, you'll need to register the autoload of the framework. I suggest to add this piece of code to your test bootstrap.

require_once 'lib/PHPTestDummy/Autoloader.php';
PHPTestDummy_Autoloader::register();

This is how to create mock class out of your mock object:

$builder = new PHPTestDummy_MockBuilder( 'OriginalClassName' );

This is not the mock object yet, but you can construct mock objects with this object, or you can get the source of the mock class for debugging purposes. The class must exist when you create the mock, because the mock object reproduces the exact signature of the original class in order to fully comply with it and prevent integration bugs in your code (eg. type hints, etc).

// Creating mock object calling the original constructor.
$mock_object = $builder->construct( $param1, $param2 );

// Getting the source of the mock class:
echo $builder->getCode();

Method construct accepts the parameters with which you would instantiate your original object. It is planned to give ability to omit constructor call.

Defining behaviours

Once the mock object is set up, it can be injected to your code. All methods are stubbed by default and return null. You can easily redefine this behaviour.

// Method methodName will always return 'abc'.
$mock_object->method( 'methodName' )->behave( 'always' )->returns( 'abc' );

You can also execute different actions, not only returning a single value.

Returning consecutive values

Consecutive values are a list of distinct values to be returned after each other, in every call. If, for ecample the list is [1, 2], the first call returns 1, the second 2, the third 1 again and so on. There values are defined per behaviour, so if you have multiple behaviours defined for a mock object, be aware of this.

// Method methodName will return 'abc' and 'def' consecutively.
$mock_object->method( 'methodName' )->behave( 'always' )->returnsConsecutive( 'abc', 'def' );

Returning one parameter of the invocation

To keep things simple, you can get make one parameter of the method invocation return. The parameters are indexed on 1 base.

// Method methodName will return the 2nd parameter of the call (234 in this case).
$mock_object->method( 'methodName' )->behave( 'always' )->returnsParameter( 2 );

var_dump( $mock_object->methodName( 123, 234 ) );

Returning a result of an arbitrary callback

If you need more complex logic, you can define the behaviour with a callback. The callback will receive the mock object as 1st parameter and the parameters of the method call along with it. If you define a public method in your test case (in an arbitrary framework), you can make assertions on the parameters too.

class RandomTestCase extends PHPUnit_Framework_TestCase 
{
    public function testExecuteCallback() 
    {
        // [...] Creating mock object.

        $mock_object->method( 'methodName' )->behave( 'always' )->executesCallback( array(
            $this,
            'callbackForMethodName'
        ) );
        
        // Obviously this is normally called from your tested object.
        $mock_object->methodName( 'abc', 'def' );
    }
    
    public function callbackForMethodName( $mock_object, $parameter1, $parameter2 )
    {
        $this->assertEquals( 'abc', $parameter1 );
        $this->assertEquals( 'def', $parameter2 );

        // Implement complex logic here.
        return $parameter1 . ' ' . $parameter2;
    }
}

Throwing exceptions

You can emulate exceptions thrown from your mocked class. This method is polimorph, can be used in 3 different ways.

// Method methodName will throw a normal Exception object.
$mock_object->method( 'methodName' )->behave( 'always' )->throws();

// Method methodName will throw a MyExceptionName object.
$mock_object->method( 'methodName' )->behave( 'always' )->throws( 'MyExceptionName' );

$e = new AnotherExceptionName;

// Method methodName will throw $e.
$mock_object->method( 'methodName' )->behave( 'always' )->throws( $e );

Executing original method from the mocked object

Although it is not a good practice, you might need to execute the original method from the object that you are mocking. You will still be able to "record" the method call, and make assertions on the data of the invocations.

// Method methodName will call the original method, just like if nothing happened.
$mock_object->method( 'methodName' )->behave( 'always' )->callsOriginal();;

Patterns of behaviours

'always' is not the only suite that you can use for behaviours. You can define multiple behaviours for one model if the don't collide. Imagine you want different behaviour for the 1st and the 2nd call.

// First call will execute original.
$mock_object->method( 'methodName' )->behave( 'first' )->callsOriginal();
// Second call will throw LogicException.
$mock_object->method( 'methodName' )->behave( '2nd' )->throws( 'LogicException' );

Here are some examples of more complex patterns.

$mock_object->method( 'methodName' )->behave( 'every 3rd' )->returns( 'a' );
$mock_object->method( 'methodName' )->behave( 'every third plus 1' )->returns( 'b' );
$mock_object->method( 'methodName' )->behave( '2nd' )->returns( 'c' );

/* 
 * [...] Executing methodName 6 times and echoing output will result:
 *
 * b
 * c
 * a
 * b
 * NULL
 * a
 *
 */

Making assertions

Once your mock object is used, you can make assertions on the stubs to see if they are executed the right times and with the correct parameters. Is is an important part, this is where you actually test the integration of yout tested object witht he mock object.

There are no constraints, you can use the assertions implemented in your test framework. It is so simple as this.

/* 
 * Example using assertions of PHPUnit.
 */

// Assert that methodName was called ont he mock object at all (at least once).
$this->assertTrue( $mock_object->method( 'methodName' )->called() );
// Assert that methodName was called exactly 3 times.
$this->assertTrue( $mock_object->method( 'methodName' )->called( 3 ) );
// Assert that methodName was called at least 2 times.
$this->assertTrue( $mock_object->method( 'methodName' )->calledAtLeast( 2 ) );

// Asserting that the first parameter of first invocation is test1.
// Notice that we use 1-based index.
$this->assertEquals( 'test1', $mock_object->method( 'methodName' )->getInvocationParameter( 1, 1 ) );
// Asserting that the first parameter of the second invocation is a string.
$this->assertInternalType( 'string', $mock_object->method( 'methodName' )->getInvocationParameter( 2, 1 ) );
// Asserting that the first parameter of the third invocation matches the regexp.
$this->assertRegExp( '/test3/', $mock_object->method( 'methodName' )->getInvocationParameter( 3, 1 ) );

If you try to access parameter or invocation that does not exist, exception is thrown.

 
blog comments powered by Disqus