47

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.

Yader Hernandez
  • 575
  • 1
  • 5
  • 5

6 Answers6

62

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.

Tyler Collier
  • 11,489
  • 9
  • 73
  • 80
David Harkness
  • 35,992
  • 10
  • 112
  • 134
  • 1
    Yup, very good solution. Inspired me to build a full-fledged stub class for cURL. Anyone for whom PHPUnit's mock and stub capabilities are not sufficient, please check out my answer below :). – Martin Ender Sep 24 '12 at 12:44
  • What if I don't want to pass the CurlRequest object to the contructor, and create it directly from within the constructor, to simplify the constructor call? How would you mock it then? – Enrique Moreno Tent Aug 15 '13 at 15:37
  • @Dbugger - Instantiate `CurlRequest` in a separate method called from the constructor. In your test case, delay calling the constructor until after you've mocked the method to return the mock request. See http://stackoverflow.com/a/5548229/285873 for an example. – David Harkness Aug 15 '13 at 16:02
  • @Dbugger - No, you cannot mock private methods with PHPUnit. You must make it protected. – David Harkness Aug 15 '13 at 16:45
  • @David Harkness I was trying to use this code and so created files for and interface and class but when I write code in my test case file as shown above, am getting warning like "1) TopupHandlerTest::testGetThrowsWhenLegacyApiIsIsBroken Trying to configure method "getInfo" which cannot be configured because it does not exist, has not been specified, is final, or is static" what mistake I am doing? can you please help – Rupal Javiya Mar 24 '18 at 05:28
  • @Rupal Are you mocking `HttpRequest` with the exact same method names? You should post a new question with your code and error message. – David Harkness Mar 25 '18 at 20:41
11

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));
    }
}
Markus Malkusch
  • 7,738
  • 2
  • 38
  • 67
  • It's rare that you would need to test a PHP built-in so directly in a test suite. Can you provide an example for overriding the curl_init() called by an application class that THEN called by the test case. – Derek Jul 29 '19 at 15:45
7

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.

cweiske
  • 30,033
  • 14
  • 133
  • 194
3

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.

Martin Ender
  • 43,427
  • 11
  • 90
  • 130
2

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.

thomasrutter
  • 114,488
  • 30
  • 148
  • 167
  • I would like to avoid doing an actually HTTP request in order to keep the tests fast. Replacing the curl functions with dummy data would be the best bet. Unfortunately, this code will be part of a framework I'm building. So anyone using it would have to install the dependency in order to run the tests correctly. – Yader Hernandez Oct 27 '11 at 04:38
  • Perhaps there is a way you can modify your application code to allow you to swap in different curl_ functions easily (using e.g. call_user_func()). Then your test code could take advantage of this? – thomasrutter Oct 27 '11 at 05:06
-1

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

Robert Martin
  • 16,759
  • 15
  • 61
  • 87
  • 2
    This will not work in every environment. If another developer runs the test and they don't have a localhost setup the test will fail for the wrong reason. – Yader Hernandez Oct 27 '11 at 04:44