Content with Style

Web Technique

Unit testing web service based models in Zend Framework

by Pascal Opitz on April 20 2009, 09:37

Web applications nowadays use an increasingly distributed set of resources. How do we test our MVC applications that use web services in their models?

What is the problem?

Unit tests for database driven models are relatively straight forward. Usually we connect to a separate database, containing a known set of data. Since we know the data we can run assertions against it.

However, unit testing models that talk to web services presents us with a different set of problem. Usually our application runs requests against something that is not in our control. Unless the service provider gives us some kind of test mode, we're operating with dynamic live data which shouldn't be used for testing, since it's harder to test in the first place, but also could be harmful to be manipulated.

Let's have a look at two strategies to test out models.

Mock objects for Services

If we are using an object to represent the API calls to the service from within our model, i.e. Zend_Service_Delicious, we can swap this object for a mock object. A simple accessor allows us to overwrite private properties of the object:


class DeliciousModel {
  private $delicious;

  public function __construct() {
    $this->delicious = new Zend_Service_Delicious('username', 'password');
  }

  public function getPosts() {
    return $this->delicious->getAllPosts();
  }

  public function setProperty($name, $value) {
    if(UNITTEST_CONTEXT) {
      $this->$name = $value;
    }
  }
}

Now it's easy swap out private properties from within a unit test:


public function testDeliciousModel {
  $mockPosts = array('foo', 'bar');

  $deliciousMock = $this->getMock('Zend_Service_Delicious', array('getAllPosts'));
  $deliciousMock->expects($this->once())
          ->method('getAllPosts')
          ->will($this->returnValue($mockPosts));

  $model = new DeliciousModel();
  $model->setProperty('delicious', $deliciousMock);

  $posts = $model->getPosts();
  $this->assertEquals($posts, $mockPosts);
}

Of course this would mean that we're not launching any requests to our services at all, but instead check that models are doing the right method calls to the service API object, and get the right data back.

Validate service requests

Generally speaking, models that are using web services will transform method calls into HTTP requests against web service end points. Those might come in a variety of flavours, but all together we can say that, if we can intercept the request, we can make sure the model serializes method calls correctly into known request data.

Test adapter for Zend_HTTP calls

If our model uses Zend_Http to launch the API calls, we might provide a hook in the model so that we can swap the HTTP adapter for the test adapter that ships with Zend Framework. We then have access to the entire request that the model launched, which we can use for assertions:


public function testJsonCall() {
  $adapter = new Zend_Http_Client_Adapter_Test();

  $response = "";
  $response .= "HTTP/1.1 200 OK" . "\r\n";
  $response .= "Content-type: application/json" . "\r\n";
  $response .= "\r\n";
  $response .= '{}';

  $adapter->setResponse($response);

  $model = new JsonModel();
  $model->setAdapter($this->adapter);

  $request_raw = $model->http_client->getLastRequest();
  // do your assertions here
}

Swap the endpoint

Instead of changing the adapter to intercept the call, we could swap the endpoint of the web service against a mock service. This endpoint then can respond with known data which we can use for our assertions.

Depending on what kind of web services we have to mock, we might use the servers provided by Zend, e.g. Zend_Rest_Server or Zend_Soap_Server.

A better understanding

Looking at the two strategies above, it still might seem to be a hassle to do, what is essentially a replication of the web service functionality with test data, whether that happens as mock object, on the request/response level, or as test endpoint.

But instead, this exercise is a very valuable documentation of ones understanding of the service functionality at a particular point in time. Given that service APIs are often in a constant state of change, things might break in the future, but the tests we've written can help us understand whether it's down to our own code or the web service instead. If things fail at the web service level, we can now refer to our tests to compare what we expect the service to do with what it actually does.

Related Links

Comments

  • Hello,

    I am trying to do the same thing as above but using Zend_Service_Twitter

    I have this:

    
    $mockReturn = array();
    $mockReturn['request'] = '/account/verify_credentials.xml';
    $mockReturn['error'] = 'Could not authenticate you.';
    
    $stub = $this->getMock('Zend_Service_Twitter', array('verifyCredentials'),array(),'',FALSE);
    
     $stub->expects($this->once())
                  ->method('verifyCredentials')
                  ->will($this->returnValue($mockReturn));
    
    $model = new Twitter_Model_Twitter(array('username'=>'test','password'=>'passwd'));
    $model->setOptions(array('twitter'=>$stub));
    
    $posts = $model->verifyCredentials();
    $this->assertEquals($posts, $mockReturn);
    

    I am confused because it is trying to access the twitter.com API. I know this because when I turned off my internet connection, I got this:

    
    1) testVerifyCredentials(Model_TwitterTest)
    Zend_Http_Client_Adapter_Exception: Unable to Connect to tcp://twitter.com:80. Error #60: Operation timed out
    

    FYI: when it connects successfully to the twitter API i get this:

    
    1) testVerifyCredentials(Model_TwitterTest)
    Failed asserting that two objects are equal.
    --- Expected
    +++ Actual
    @@ -1,9 +1,5 @@
    -Zend_Rest_Client_Result Object
    +Array
     (
    -    [_sxml:protected] => SimpleXMLElement Object
    -        (
    -            [request] => /statuses/friends.xml
    -            [error] => This method requires authentication.
    -        )
    -
    +    [request] => /account/verify_credentials.xml
    +    [error] => Could not authenticate you.
     )
    
    /htdocs/twitter/tests/application/models/TwitterTest.php:57
    

    How do I properly test Zend_Service_Twitter? When my Twitter_Model_Twitter looks like this:

    by wenbert on August 10 2009, 05:18 #

  • Hello,

    You have a typo in the first code example [...]

    by Jeremy Glover on April 29 2009, 16:37 #

  • Fixed, thanks for spotting it.

    by Pascal Opitz on April 29 2009, 16:44 #

  • Wenbert, it looks to me like the twitter object doesn't swap properly in your model. If I was you I'd var_dump the model after I injected the mock object, to verify what's going on, and take it from there.

    Also I'd have a close look at what exactly you are doing in $model->setOptions. Refer to the code above, there it's called $model->setProperty. Does this successfully alter the property? Again a var_dump before and after the replacement will probably show this.

    If this part works, you'll probably have to check whether you overwrite the 'twitter' property later on.

    Finally I'd recommend you an HTTP tracker like HTTPScoop to see what requests your machine does to the outside. It helps to have a close eye on the HTTP traffic when working with web services.

    Good luck!

    by Pascal Opitz on August 10 2009, 06:43 #

  • Some nice examples of mocking techniques for web services. Thanks.

    by David Weinraub on September 24 2009, 13:34 #