如何在PHPUnit中模拟外部Web请求?

gog*_*n13 7 php phpunit unit-testing

我正在使用PhactoryPHPUnit为PHP Propel项目设置测试套件.我目前正在尝试对发出外部请求的函数进行单元测试,并且我希望在该请求的模拟响应中存根.

这是我试图测试的类的片段:

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);
  }
  ...
Run Code Online (Sandbox Code Playgroud)

这是我试图写的测试功能.

// 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);
}
Run Code Online (Sandbox Code Playgroud)

在我尝试测试代码后,我现在可以看到,我正在创建一个模拟对象getMock,然后从不使用它.所以函数fetchUrl 实际执行,我不想要.但我仍然希望能够使用Phactory创建的endpoint对象,因为它具有从我的工厂定义填充的所有正确的字段.

有没有办法在现有对象上存根方法?所以我可以存根 刚刚创建fetch_url$endpointEndpoint对象?

或者我说这一切都错了; 有没有更好的方法让我对依赖外部Web请求的函数进行单元测试?

我确实阅读了有关"Stubbing and Mocking Web Services"的PHPUnit文档,但他们这样做的示例代码是40行,不包括必须定义自己的wsdl.我很难相信这是我处理这个问题最方便的方式,除非SO的优秀人士强烈反对.

非常感谢任何帮助,我整天都被挂了.谢谢!!

Sgo*_*kes 12

从测试的角度来看,您的代码有两个问题:

  1. 网址是硬编码的,让您无法进行开发,测试或生产
  2. 端点知道如何检索数据.从你的代码我不能说端点真正做了什么,但如果它不是一个低级别的"Just get me Data"对象,它应该不知道如何检索数据.

使用这样的代码,没有好的方法来测试代码.您可以使用Reflections,更改代码等.这种方法的问题在于,您不会测试您的实际对象,而是一些反射,这些反射可以通过测试进行更改.

如果你想编写"好"的测试,你的端点应该是这样的:

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);
        // ...
    }
}
Run Code Online (Sandbox Code Playgroud)

现在你可以注入一个DataParser模拟器,它会根据你想要测试的内容返回一些默认响应.

下一个问题可能是:如何测试DataParser?大多数情况下,你没有.如果它只是php标准函数的包装器,则不需要.您的DataParser应该非常低级,如下所示:

class DataParser {
    public function fetchUrl($url) {
        return file_get_contents($url);
    }
}
Run Code Online (Sandbox Code Playgroud)

如果您需要或想要测试它,您可以创建一个Web服务,它存在于您的测试中并充当"模拟",始终返回预配置的数据.然后,您可以调用此模拟URL而不是真实的url并评估返回.

  • 这就是我最终做的事情,尽管我对此并不感到兴奋.另一个类只是为了包装`file_get_contents` _feels_就像overkill一样.这是一个代码更改,我只会测试它,这对我感觉不错.我来自ruby,它有[webmock](https://github.com/bblimke/webmock/)gem,它就像你在上一段中描述的那样:"创建一个存在于你的测试中的Web服务充当"模拟",总是返回预先配置的数据".代替自己编码,我现在将去模拟对象路线.谢谢你的想法! (2认同)
  • 我不认为这是开销.想想这样:你将在很多地方使用这个代码.从现在开始,有一种方法可以更好地实现file_get_contents,你必须在任何地方进行更改.或者认为你想切换到Buzz(我推荐).在上面的代码中,您注入了对象并且必须更改一个方法调用.Mayvbe在你的小例子中是开销,但随着你的应用程序变得越来越大,你可能想要重用file_get_contents,你可能会很感激. (2认同)