Yader Hernandez
Yader Hernandez

Reputation: 605

how to unit test curl call in php

How would you go about unit testing a curl implementation?

  public function get() {
    $ch = curl_init($this->request->getUrl());

    curl_setopt($ch, CURLOPT_HEADER, false);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

    $result = curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
    curl_close($ch);

    if (!strstr($type, 'application/json')) {
      throw new HttpResponseException('JSON response not found');
    }

    return new HttpResponse($code, $result);
  }

I need to test the content type returned so that it can throw an exception.

Upvotes: 50

Views: 41145

Answers (6)

Markus Malkusch
Markus Malkusch

Reputation: 7868

You might use a function mock library. I made one for you: php-mock-phpunit

namespace foo;

use phpmock\phpunit\PHPMock;

class BuiltinTest extends \PHPUnit_Framework_TestCase
{

    use PHPMock;

    public function testCurl()
    {
        $curl_exec = $this->getFunctionMock(__NAMESPACE__, "curl_exec");
        $curl_exec->expects($this->once())->willReturn("body");

        $ch = curl_init();
        $this->assertEquals("body", curl_exec($ch));
    }
}

Upvotes: 12

David Harkness
David Harkness

Reputation: 36532

As thomasrutter suggested, create a class to abstract the usage of the cURL functions.

interface HttpRequest
{
    public function setOption($name, $value);
    public function execute();
    public function getInfo($name);
    public function close();
}

class CurlRequest implements HttpRequest
{
    private $handle = null;

    public function __construct($url) {
        $this->handle = curl_init($url);
    }

    public function setOption($name, $value) {
        curl_setopt($this->handle, $name, $value);
    }

    public function execute() {
        return curl_exec($this->handle);
    }

    public function getInfo($name) {
        return curl_getinfo($this->handle, $name);
    }

    public function close() {
        curl_close($this->handle);
    }
}

Now you can test using a mock of the HttpRequest interface without invoking any of the cURL functions.

public function testGetThrowsWhenContentTypeIsNotJson() {
    $http = $this->getMock('HttpRequest');
    $http->expects($this->any())
         ->method('getInfo')
         ->will($this->returnValue('not JSON'));
    $this->setExpectedException('HttpResponseException');
    // create class under test using $http instead of a real CurlRequest
    $fixture = new ClassUnderTest($http);
    $fixture->get();
}

Edit Fixed simple PHP parse error.

Upvotes: 64

Martin Ender
Martin Ender

Reputation: 44259

I stumbled upon this question when I was trying to test a class using cURL myself. I took David Harkness's advice to heart and created an interface for cURL. However, the stub/mock functionality provided by PHPUnit was not sufficient in my case, so I added my own stub implementation of the interface and put it all on GitHub. And because this question shows up rather early on Google when searching this issue, I thought I would post it here, so the others might be able to save the effort.

Here it is.

The repository's wiki contains a rather detailed documentation of the stub's capabilities, but here they are in short.

The interface is a 1:1 mapping of PHP's cURL functions, so as to make it very easy to start using the interface (simply hand your ClassUnderTest an instance implementing SAI_CurlInterface and then call all cURL functions as before, but as methods on that instance). The class SAI_Curl implements this interface by simply delegating to cURL. Now if you want to test the ClassUnderTest you can give it an instance of SAI_CurlStub.

The stub mainly alleviates the problem that PHPUnit's mocks and stubs cannot returned dummy data depending on former function calls (but this is how cURL actually works - you set up your options and the response, error code and cURL-info depend on those options). So here is a short example, showing those capabilities for responses (for error codes and cURL-info, see the wiki).

public function testGetData()
{
    $curl = new SAI_CurlStub();

    // Set up the CurlStub
    $defaultOptions = array(
        CURLOPT_URL => 'http://www.myserver.com'
    );
    $chromeOptions = array(
        CURLOPT_URL => 'http://www.myserver.com',
        CURLOPT_USERAGENT => 'Chrome/22.0.1207.1'
    );
    $safariOptions = array(
        CURLOPT_URL => 'http://www.myserver.com',
        CURLOPT_USERAGENT => 'Safari/537.1'
    );

    $curl->setResponse('fallback response');
    $curl->setResponse('default response from myserver.com'
                       $defaultOptions);
    $curl->setResponse('response for Chrome from myserver.com',
                       $chromeOptions);
    $curl->setResponse('response for Safari from myserver.com',
                       $safariOptions);

    $cut = new ClassUnderTest($curl);

    // Insert assertions to check whether $cut handles the
    // different responses correctly
    ...
}

You can make your response dependent on any combination of any cURL-options. Of course, you can take this even further. Say for example, your ClassUnderTest takes some XML data from a server and parses it (well, you should have two separate classes for those tasks, but let's assume this for our example), and you want to test that behavior. You could download the XML response manually, and have your test read the data from the file and stuff it into the response. Then you know exactly what data is there, and can check whether it's parsed correctly. Alternatively, you could implement the SAI_CurlInterface loading all responses from your file system right away, but the existing implementation is definitely a point to start.

At the time that I I am writing this answer, @SAI_CurlStub@ does not support cURL multi-lib features yet, but I plan to implement this, too, in the future.

I hope this stub is of help to anyone who wants to unit test cURL-dependent classes. Feel free to check out and use the classes, or contribute, of course - it's on GitHub after all :). Also, I am open to any constructive criticism regarding implementation and usage of interface and stub.

Upvotes: 3

cweiske
cweiske

Reputation: 31078

Do not use curl directly but through a wrapper like PEAR's HTTP_Request2. With it, you have the ability to exchange the curl driver with a mock driver - ideal for unit tests.

Upvotes: 7

thomasrutter
thomasrutter

Reputation: 117333

One approach to this involves replacing the interface you are using (in this case, the curl_ functions) with dummy versions of themselves which return certain values. If you were using an object-oriented library this would be easier because you could just substitute an dummy object which has the same method names (and indeed, frameworks like simpletest can set up dummy object methods easily). Otherwise, perhaps there is some other sorcery you can use to override built-in functions with dummies. This extension includes override_function() which looks like what you'd need, though that would add another dependency.

If you want to test this without replacing the curl_ functions with dummy versions, it looks like you will need to set up a dummy server that will return a certain result, so that you can test the way your PHP, and its curl extension, handles that result. To fully test it, you'd need to access this over HTTP rather than, say, a local file, because your PHP depends on having an HTTP response code, etc. So your tests will need a functioning HTTP server.

Incidentally, PHP 5.4 will actually include its own web server which would come in handy for this purpose. Otherwise, you could put a test script on a known server which you control, or distribute a simple server config with your tests.

If you were to actually use the live server for your testing, this would become less of a unit test and more of an integration test, because you be testing both your PHP and the server, and the integration between the two. You would also miss out on being able to test on demand how your code handles certain failures.

Upvotes: 2

Robert Martin
Robert Martin

Reputation: 17157

In your unit test, have request->getUrl() return the URI of a local file that you know will throw the exception.

Upvotes: -1

Related Questions