Book Review: How to Implement Design Patterns in PHP
Using Multi-Byte Character Sets in PHP (Unicode, UTF-8, etc)
Using PHP pspell Spell Check Functions with a Custom Dictionary
A Simple ISAPI Filter for Authentication on IIS
ENUMs, User Preferences, and the MySQL SET Datatype
JQuery Venetian Blinds Transition Effect

Mocking External Services with PHPUnit

Tuesday, 20 March 12, 6:34 pm
The basic idea behind unit testing is that the different components of an application can be tested individually (as a unit no less) by calling each component from a test application passing in a range of suitably picked parameters. When these automated tests fail, the idea is that they will pinpoint where the problem lies and what causes it.

Unit testing is naturally suited to OOP systems, as classes are very modular components that can be readily instantiated and tested in isolation by the test application. Additionally, the concept of inheritance can be leveraged to allow test code to gain access to the internals of a class even when they are marked protected, and reflection can be used to examine the values of private properties.

The big advantage of automated tests is their repeatability: implemented as part of a deployment process they can highlight code problems without developer interaction. Even if the tests are run manually, they offer uniformity in testing. They do certainly add to the development timeframe, and have a small overhead when releasing code, but the advantage of avoiding code releases without proper testing when, for instance, code is assumed to be sound because it was tested just prior to an apparently minor change.

When we write unit tests, we are generally testing that a class behaves as expected given certain inputs. For instance, a collection class should have a size of zero immediately after being created, and as further elements are added the size should change accordingly. The tests we choose to create are specific to the code under test. If our collection acted a bit like a stack for example, we'd also want our test code to check that the order of items retrieved from the collection matches our expectations based on the order in which they were added.

Mocking Methods

When testing a class that consumes an external service, the best design approach is to place the code which calls the external API in a single protected method, and then call that method from other methods in your code when they need to use the services of the API.

Mocking such methods is pretty straightforward. When instantiating the class under test in the setUp() method, use the getMock() method of your test class (inherited from the superclass), with the second argument an array of methods that are to be mocked:
$this->_object = $this->getMock('ClassUnderTest', array('_callAPImethod'));
This example shows how you could use a mock to replace a method in the class under test. You can however create a mock for any class you like - so if you are using an external class to talk to an API, you could mock that class by naming it in the call to getMock(). The second argument to getMock() is an array listing the methods that you will be replacing (if you don't provide this argument, all methods of the mock object are stubbed to return null). The third argument allows you to specify parameters to be passed to the constructor of the mocked class, as an array of values.

Defining the Behaviour of Mocked Methods

In your test methods, you configure what the mocked method should return under particular circumstances, using the methods expects(), method() and will(), in combination with methods such as returnValue(), returnValueMap(), or returnSelf():
$this->_object->expects($this->any())->method('_callAPImethod')->will($this->returnValue(10));
What the above does is configure the mock object such that the _callAPImethod() will return the value 10, for any parameters. Note that instead of the number 10, we could have any PHP value, from strings, to arrays, even an object reference.

The other methods that we could use are:
returnValue($val)Always return the same value
onConsecutiveCalls($val1, $val2, .. $valn)The first time the mocked method is called, $val1 is returned, the second time $val2 is returned, and so on.
returnValueMap($map)The map is a 2D array of arrays, where the first elements of each subarray are values that must match the arguments for the remaining element to be used as return value
throwException($exception)When called, the stubbed method will throw the exception specified in the argument
returnArgument(int $offset)Return the method argument specified by its position in the argument list
returnSelf()Return the instance of the class under test
returnCallback('callback_function')The stub's return value is calculated by the named callback function

Mocking with a Callback Function

The returnCallback('callback_function') method will create a mock that returns a value determined at runtime by the specified callback function. The callback function is passed a cloned copy of the parameters received by the method in the class under test (note they are not 'live' references so you can't make any persistent changes to them in the callback). Here's how you might use it with a closure (the closure requires PHP 5.3 on your testing server):
$this->returnCallback( function($parameter) { $retval = false; switch ($parameter) { case 'some value': $retval = 6; break; case 'another value': $retval = 5; break; default: $retval = 0; } });

PHPUnit's MySQL Dataset

Starting from version 3.5, PHPUnit added support for MySQL's XML data description format, as generated by mysqldump with the --xml (or -X) flag. This makes creating datasets for use with PHPUnit tests much simpler and less errorprone. Create a dataset by running mysqldump like so:
mysqldump -Xtu dbuser -p databaseName table1Name table2Name > /path/to/dataset.xml
Note that we're using the -t flag to skip the XML table definition, and only get XML for each table row. You can also use the --where option to limit the returned rows to those matching a specified condition — useful for getting a small set of test data from a live database:
mysqldump -Xtu dbuser -p --where='column IN ("value1", "value2")' databaseName table1Name table2Name > /path/to/dataset.xml
We create an internal dataset from MySQL's raw XML by passing the filepath into createMySQLXMLDataSet(), in place of createXMLDataSet(). The datasets we get can of course be used in just the same way as datasets generated from PHPUnit's own XML format, for instance they can be filtered by passing them into PHPUnit_Extensions_Database_DataSet_DataSetFilter():
$dataSet = $this->createMySQLXMLDataSet(dirname(__FILE__) . '/datasets/testAddNewSupplier.xml'); $expectedDataSet = new PHPUnit_Extensions_Database_DataSet_DataSetFilter($dataSet, array('suppliers' => array('created')));
Alternatively, you could of course just edit the XML to remove any unwanted columns by hand. Once you have the dataset that you expect your test to return, you can compare it with the actual dataset the test produced using just the same functions as with a standard dataset, such as assertDataSetsEqual():
$dataSet = $this->_dbDefault->createDataSet(array('suppliers', 'products')); $actualDataSet = new PHPUnit_Extensions_Database_DataSet_DataSetFilter($dataSet, array('suppliers' => array('created')));   // Check that they are the same $this->assertDataSetsEqual($expectedDataSet, $actualDataSet);
So when you're writing new tests, first of all, before running any tests, create the rows in your database that you want to use as the starting point. You could do this by creating dummy rows or by importing selected rows from a full database. Run mysqldump -X to grab the XML that you can then load in your test class's getDataset() method. Next run the first test on its own (by using phpunit's --filter or --group arguments, or failing that by placing an exit at the end of the first test), and afterwards, grab a dump again, and load this in your first test as the expected dataset using createMySQLXMLDataSet().

Please enter your comment in the box below. Comments will be moderated before going live. Thanks for your feedback!

Cancel Post

/xkcd/ Arizona Chess