Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I mock an external web request in PHPUnit?

I am working on setting up a testing suite for a PHP Propel project using Phactory, and PHPUnit. I am currently trying to unit test a function that makes an external request, and I want to stub in a mock response for that request.

Here's a snippet of the class I am trying to test:

class Endpoint {
  ...
  public function parseThirdPartyResponse() {
    $response = $this->fetchUrl("www.example.com/api.xml");
    // do stuff and return 
    ...
  }

  public function fetchUrl($url) {
    return file_get_contents($url);
  }
  ...

And here's the test function I am trying to write.

// my factory, defined in a seperate file
Phactory::define('endpoint', array('identifier'  => 'endpoint_$n');

// a test case in my endpoint_test file
public function testParseThirdPartyResponse() {
  $phEndpoint = Phactory::create('endpoint', $options);
  $endpoint = new EndpointQuery()::create()->findPK($phEndpoint->id);

  $stub = $this->getMock('Endpoint');
  $xml = "...<target>test_target</target>...";  // sample response from third party api

  $stub->expects($this->any())
       ->method('fetchUrl')
       ->will($this->returnValue($xml));

  $result = $endpoint->parseThirdPartyResponse();
  $this->assertEquals('test_target', $result);
}

I can see now, after I tried my test code, that I am creating a mock object with getMock, and then never using it. So the function fetchUrl actually executes, which I do not want. But I still want to be able to use the Phactory created endpoint object, since it has all the right fields populated from my factory definition.

Is there a way for me to stub a method on an existing object? So I could stub fetch_url on the $endpoint Endpoint object I just created?

Or am I going about this all wrong; is there a better way for me to unit test my functions that rely on external web requests?

I did read the PHPUnit documentation regarding "Stubbing and Mocking Web Services", but their sample code for doing so is 40 lines long, not including having to define your own wsdl. I'm hard pressed to believe that's the most convenient way for me to handle this, unless the good people of SO feel strongly otherwise.

Greatly appreciate any help, I've been hung up on this all day. Thanks!!

like image 493
goggin13 Avatar asked Jun 29 '12 19:06

goggin13


1 Answers

From a testing perspective, your code has two problems:

  1. The url is hardcoded, leaving you no way of altering it for development, testing or production
  2. The Endpoint knows about how to retrieve data. From your code I cannot say what the endpoint really does, but if it's not a low level "Just get me Data" object, it should not know about how to retrieve the data.

With your code like this, there is no good way to test your code. You could work with Reflections, changing your code and so on. The problem with this approach is that you don't test your actual object but some reflection which got change to work with the test.

If you want to write "good" tests, your endpoint should look something like this:

class Endpoint {

    private $dataParser;
    private $endpointUrl;

    public function __construct($dataParser, $endpointUrl) {
        $this->dataPartser = $dataParser;
        $this->endpointUrl = $endpointUrl;
    }

    public function parseThirdPartyResponse() {
        $response = $this->dataPartser->fetchUrl($this->endpointUrl);
        // ...
    }
}

Now you could inject a Mock of the DataParser which returns some default response depending on what you want to test.

The next question might be: How do I test the DataParser? Mostly, you don't. If it is just a wrapper around php standard functions, you don't need to. Your DataParser should really be very low level, looking like this:

class DataParser {
    public function fetchUrl($url) {
        return file_get_contents($url);
    }
}

If you need or want to test it, you could create a Webservice which lives within your tests and acts as a "Mock", always returning preconfigured data. You could then call this mock url instead of the real one and evaluate the return.

like image 141
Sgoettschkes Avatar answered Sep 22 '22 14:09

Sgoettschkes