Linux and PHP web application support and development (Bromsgrove, UK)

tel: 0845 0046746

PHPUnit testing – mock and double objects

When writing unit tests, it’s often desirable to ‘mimick’ a backend call to web service or database (without using the real thing backend). Ideally you need this to be quick and easy to do. One way of doing this is to use mock or double objects. This article discusses their use with PHPUnit (a unit testing framework for PHP).

In some circumstances, a library or component may provide a mock or test backend (for example, with the Zend_Http_Client, a test adapter can be plugged in which allows you to pre-set a given response, like in example 1 below). In some circumstances, however, you may not have access to a test adapter, or it may not be possible to inject dependencies into the necessary objects.

The below article shows how you can use hard-coded mock objects (Zend_Http_Client_Adapter_Test), or allow PHPUnit to dynamically create one for you using its test double functionality.

The example code below is based around sending push notifications to the Android cloud messaging platform – for android devices. A service (not included in full) has already been written (Mobile_AndroidPushMessage) which uses Zend_Http_Client internally to send the push message. Its API is similar to the below :

class Mobile_AndroidPushMessage {
     public function addTo($recipientKey);
     public function setPayload($data);
     public function setHttpClient(Zend_Http_Client $client);
     public function send(); // returns boolean
     public function getErrors(); // return array or null if no errors
}

Using a using Zend_Http_Client_Adapter_Test

The Zend_Http_Client library allows you to inject in a different backend adapter – so rather than making an actual request to Google, we can use a pre-determined one. This ensures our test is repeatable and predictable – and fast.

So, firstly, let’s setup a contrived PHPUnit test with a ‘fake’ success message ….

// .... class spec etc.
public function setUp() { 
    $m = new Mobile_AndroidPushMessage();
    $httpClient = new Zend_Http_Client();
    $successAdapter = new Zend_Http_Client_Adapter_Test();
    $successAdapter->setResponse("HTTP/1.1 200 OK \r\nContent-Type: application/json\r\n\r\n" .
       '{"multicast_id":579055555555471,"success":1,"failure":0,"canonical_ids":0,"results":[{"message_id":"0:13612757325555555555d6f9fd7ecd"}]}');
    $httpClient->setAdapter($successAdapter);
    $m->setHttpClient($httpClient);
    $this->androidGcmService = $m; // store it for later use by tests
}
  ...

And test is as follows :

public function testBasicBehaviour() {
    // ... setup payload/recipient key data, can be anything in this example. 
    $m = $this->androidGcmService;
    $m->setPayload('message');
    $m->addTo('someone');
    $ok = $m->send();
    $this->assertTrue($ok, "always returns true, as setUp contains a hard coded success message!");
}

And, as we control the response, we can easily change the above to mimic a failure at the GCM end, using :

public function testErrorHandling() { 
    $m = $this->androidGcmService;
    $adapter = $m->getHttpClient()->getAdapter();
    $adapter->setResponse('HTTP/1.1 200 OKr\nContent-Type: application/json\r\n\r\n I am not json, so something blows up here');
    $m->setPayload('whatever');
    $m->addto('someone');
    $ok = $m->send();
    $this->assertFalse($ok);
    $errors = $m->getErrors();
    // Check contents of the 'errors' array.
}

So – using the Test adapter we can fake error conditions from Google and test our code in isolation.

Using a mock object / test double around Zend_Http_Client

Sometimes we may not always have a test adapter to pass in, or we may instead want to test that (for example) a single HTTP Post request is made. Often a library will not be structured in a manner which lends itself for unit tests, and you may often find yourself needing to sub-class the library and modify parts of its behaviour – however this is probably error prone and time consuming. Thankfully, PHPUnit can help – with its test double support (see here for PHPUnit’s docs on this)

A basic example follows –

public function testPostOnce() {
    $m = $this->androidGcmService;
    // create the doppleganger Zend_Http_Client object, only mocking the 'request' method
    // other methods will function as expected.
    $mock = $this->getMock('Zend_Http_Client'), array('request'));
    $http_body_text = 'some.json.goes.here';
    $fakeResponse = new Zend_Http_Response('200', array(), $http_body_text);
    // tell PHPUnit that the 'request' method should only be called once, and must be a HTTP POST.
    $mock->expects($this->once())->method('request')->with($this->equalsTo('POST'))->will($this->returnValue($fakeResponse));
    $m->setHttpClient($mock);
    // set recipients & payload etc.
    $ok = $m->send();
    $this->assertTrue($ok);
}

When we call the ‘getMock’ function we provide the name of the class we which to ‘extend’. By default, PHPUnit will mock all methods of the specified class – and calling them will return null. In our case we only want to mock one method – ‘request’, and leave all other methods unchanged.

So, the test checks that our class will call the request method once (and once only) and, when doing so, there will be one argument (‘POST’). When the method is called, it will return a pre-populated ‘fake’ response ($fakeResponse).

PHPUnit is doing some magic behind the scenes when creating the mock/double instance, in that it is extending the parent (Zend_Http_Client). This is good – it allows our type hints to function correctly (setHttpClient expects a class of type Zend_Http_Client) and redefines the request() method.

The above is much easier than us creating a new class extending Zend_Http_Client just for the purposes of this test.

As an example, if the Mobile_AndroidPushMessage class actually made a GET request by ‘mistake’ you’d see output like :

Expectation failed for method name is equal to when invoked 1 time(s)
Parameter 0 for invocation Zend_Http_Client::request(‘POST’) does not match expected value.
Failed asserting that two strings are equal.
— Expected
+++ Actual
@@ @@
-‘GET’
+’POST’

Using the above, we can ensure that the Http Client is called once, check the parameters used and specify the return value. Again, this is without depending on the actual Google service.

, , ,

Leave a Reply

Your email address will not be published. Required fields are marked *