Zend Framework testing: emulating HTTP calls

Following last month's article by Ian, here's some thoughts on how to test a Zend Framework application.

One of the unit testing best practices suggests to break dependencies, so you can test each component separately.

The first problem that arises when you want to test controllers might be having a tighter control over the HTTP Request and Response objects. Fortunately, ZF already has something that really makes your life easier, i.e. the Zend_Test_PHPUnit_ControllerTestCase class, which has stubs for the Request and Response objects, and you can easily check headers, return codes, routes, redirects, and even the view itself.
If you haven't tried it yet, do yourself a favor and give it a go.

Once you're familiar with Zend_Test_PHPUnit_ControllerTestCase, you should not have any more problems with testing the controllers. The only caveat is that sometimes, when there are conditional redirects in your actions, it's not easy to track the actual route: if your test assertions fail, trust them and pay closer attention to what the program flow really is.

Let's now see how we can tackle the models.
Typically, this is one of the more problematic areas to test since they almost always have external dependencies (file system, database, mail, etc.). You can "fake" most of these dependencies with some well known tools, like FakeMail or PEAR::Mail_mock, QuerySims, or write your own fake file system (PDF link).

But what if your model is calling a web service?
Remember that what you want to check is the behavior of the model itself, depending on the web service response, and not both the model AND the web service together.

Here's how Dependency Injection comes into play (if you haven't read Ian's article yet, do so now).

The simplest way of detaching the model from the web services is to have an HTTP stub, and use it instead of the "real" HTTP client during tests.

We start by creating the HTTP stub class:

<?php

/**
 * Stub for the Zend_Http_Client class to fake REST-style responses
 */
class HttpClientStub extends Zend_Http_Client
{
    /**
     * @var string
     */
    private $_expected;

    /**
     * Send the HTTP request and return an HTTP response object
     *
     * @param string $method
     *
     * @return Zend_Http_Response
     * @throws Zend_Http_Client_Exception
     */
    public function request($method = null) {
        $headers = array(
            'Date'           => 'Thu, 31 Jul 2008 16:44:38 GMT',
            'Server'         => 'Apache/2.2.8 (EL)',
            'X-powered-by'   => 'PHP/5.2.6',
            'Content-length' => strlen($this->_expected),
            'Connection'     => 'close',
            'Content-type'   => 'text/xml',
        );
        return new Zend_Http_Response(200, $headers, $this->_expected);
    }

    /**
     * Set the expected value for the following request() call
     *
     * @param string $value
     *
     * @return string
     */
    public function setExpected($value) {
        $this->_expected = $value;
    }
}

Then we add a couple of hooks in our model to inject the HTTP client:

<?php

class MyModel
{
    /**
     * @var Zend_Http_Client
     */
    protected static $_httpClient = null;

    /**
     * Set a Zend_Http_Client instance
     * @param Zend_Http_Client $httpClient HTTP client instance
     * @return void
     */
    public static function setHttpClient(Zend_Http_Client $httpClient) {
        self::$_httpClient = $httpClient;
    }

    /**
     * Get a Zend_Http_Client instance
     * @return Zend_Http_Client
     */
    protected static function getHttpClient() {
        if (!self::$_httpClient instanceof Zend_Http_Client) {
            self::$_httpClient = new Zend_Http_Client();
        }
        return self::$_httpClient;
    }
      
    /**
     * An actual method of the model class...
     *
     * @param string $param some parameter
     *
     * @return string
     */
    public function doSomething($param) {
        $client = self::getHttpClient()->setUri('http://my.web.service/methodname');
        $client->setParameterPost('param', $param);
        $client->setEncType(Zend_Http_Client::ENC_URLENCODED);
        $response = $client->request(Zend_Http_Client::POST);
        
        //...
    }
}

If you don't call setHttpClient() with your object instance, Zend_Http_Client is used by default, so your application will continue working exactly as it used to. The difference is that now you can use another HTTP client (e.g. the stub we created before) in your test cases:

<?php

public function testDoSomething()
{       
    $stub = new HttpClientStub();
    // $this->_object is an instance of your MyModel class
    $this->_object->setHttpClient($stub);

    // set the expected response
    $stub->setExpected('<response status="ok"><value>Some Value</value></response>');
    
    // call the model method you want to test
    $res = $this->_object->doSomething('some parameter');
    
    // test the response
    $this->assertEquals('expected result', $res);
}

Of course, you want to repeat the call with different web service responses, so you can cover all the possible conditions of your app. Now, we might want to take it one step further. If your model method calls two or more web services, you clearly need to pass multiple fake responses to the stub HTTP client.

To keep things simple, we can modify the HTTP Stub class and add a queue for the expected responses:

<?php

class HttpClientStub extends Zend_Http_Client
{
    /**
     * @var array
     */
    private $_expected = array();

    /**
     * Send the HTTP request and return an HTTP response object
     *
     * @param string $method
     * @return Zend_Http_Response
     * @throws Zend_Http_Client_Exception
     */
    public function request($method = null) {
        $expected = array_shift($this->_expected);
        $headers = array(
            'Date'           => 'Thu, 31 Jul 2008 16:44:38 GMT',
            'Server'         => 'Apache/2.2.8 (EL)',
            'X-powered-by'   => 'PHP/5.2.6',
            'Content-length' => strlen($expected),
            'Connection'     => 'close',
            'Content-type'   => 'text/xml',
        );
        return new Zend_Http_Response(200, $headers, $expected);
    }

    /**
     * Set the expected value for the following request() call
     *
     * @param string $value
     *
     * @return string
     */
    public function setExpected($value) {
        $this->resetExpected();
        $this->addExpected($value);
    }

    /**
     * Set the expected value for the next request() call in the queue
     *
     * @param string $value
     *
     * @return string
     */
    public function addExpected($value) {
        $this->_expected[] = $value;
    }
    
    /**
     * Reset the expected responses
     *
     * @return void
     */
    public function resetExpected() {
        $this->_expected = array();
    }
}

This way, if your doSomething() method contains more than one web service call in its body, you can set the expectations for each one of them:

<?php

public function testDoSomething()
{       
    $stub = new HttpClientStub();
    // $this->_object is an instance of your MyModel class
    $this->_object->setHttpClient($stub);

    // set the expected response for each web service call within the doSomething() method
    $stub->setExpected('<response status="ok"><value>Some Value</value></response>');
    $stub->addExpected('<response status="ok"><items><item>Some item</item></items></response>');
    
    // call the model method you want to test
    $res = $this->_object->doSomething('some parameter');
    
    // test the response
    $this->assertEquals('expected result', $res);
}

I hope these notes were useful to some of you. I'd love to hear how you are solving the same problem, so feel free to send me your feedback.




1 response to "Zend Framework testing: emulating HTTP calls"

Very Nicely done. We may to do some of this soon, good read

Lorenzo Alberton

Lorenzo Alberton Lorenzo PHP5 ZCE - Zend Certified Engineer has been working with large enterprise UK companies for the past years and is now CTO at DataSift. He's an international conference speaker and a long-time contributor to many open source projects. Lorenzo Alberton's profile on LinkedIN View Lorenzo Alberton's Twitter stream

Lorenzo Alberton - Sun Certified MySQL 5 Developer

Tags

AJAX, Apache, Book Review, Charset, Cheat Sheet, Data structures, Database, Firebird SQL, Hadoop, Imagick, INFORMATION_SCHEMA, JavaScript, Kafka, Linux, Message Queues, mod_rewrite, Monitoring, MySQL, NoSQL, Oracle, PDO, PEAR, Performance, PHP, PostgreSQL, Profiling, Scalability, Security, SPL, SQL Server, SQLite, Testing, Tutorial, TYPO3, Windows, Zend Framework

Buy me a book - Applied Mathematics For Database Professionals