如何编写集成测试以与外部API进行交互?

Jes*_*ord 71 api rest phpunit integration-testing unit-testing

首先,我的知识在哪里:

单元测试是测试一小段代码的方法(主要是单一方法).

集成测试是测试多个代码区域之间交互的测试(希望它们已经有了自己的单元测试).有时,部分测试代码需要其他代码以特定方式执行操作.这就是Mocks&Stubs的用武之地.因此,我们非常具体地模拟/删除部分代码.这使我们的集成测试可以预测地运行而没有副作用.

所有测试都应该能够独立运行而无需数据共享.如果需要数据共享,这表明系统没有足够的分离.

接下来,我面临的情况:

当与外部API(特别是将使用POST请求修改实时数据的RESTful API)进行交互时,我理解我们可以(应该?)模拟与该API的交互(在此答案中更加雄辩地说明)进行集成测试.我也理解我们可以单元测试与该API交互的各个组件(构造请求,解析结果,抛出错误等).我没有得到的是如何实际解决这个问题.

所以,最后:我的问题.

如何测试与具有副作用的外部API的交互?

一个完美的例子是Google的Content API用于购物.为了能够执行手头的任务,它需要大量的准备工作,然后执行实际请求,然后分析返回值.其中一些没有任何"沙盒"环境.

执行此操作的代码通常具有相当多的抽象层,例如:

<?php
class Request
{
    public function setUrl(..){ /* ... */ }
    public function setData(..){ /* ... */ }
    public function setHeaders(..){ /* ... */ }
    public function execute(..){
        // Do some CURL request or some-such
    }   
    public function wasSuccessful(){
        // some test to see if the CURL request was successful
    }   
}

class GoogleAPIRequest
{
    private $request;
    abstract protected function getUrl();
    abstract protected function getData();

    public function __construct() {
        $this->request = new Request();
        $this->request->setUrl($this->getUrl());
        $this->request->setData($this->getData());
        $this->request->setHeaders($this->getHeaders());
    }   

    public function doRequest() {
        $this->request->execute();
    }   
    public function wasSuccessful() {
        return ($this->request->wasSuccessful() && $this->parseResult());
    }   
    private function parseResult() {
        // return false when result can't be parsed
    }   

    protected function getHeaders() {
        // return some GoogleAPI specific headers
    }   
}

class CreateSubAccountRequest extends GoogleAPIRequest
{
    private $dataObject;

    public function __construct($dataObject) {
        parent::__construct();
        $this->dataObject = $dataObject;
    }   
    protected function getUrl() {
        return "http://...";
    }
    protected function getData() {
        return $this->dataObject->getSomeValue();
    }
}

class aTest
{
    public function testTheRequest() {
        $dataObject = getSomeDataObject(..);
        $request = new CreateSubAccountRequest($dataObject);
        $request->doRequest();
        $this->assertTrue($request->wasSuccessful());
    }
}
?>
Run Code Online (Sandbox Code Playgroud)

注意:这是一个PHP5/PHPUnit示例

鉴于这testTheRequest是测试套件调用的方法,该示例将执行实时请求.

现在,这个实时请求(希望提供一切顺利)执行POST请求,其具有更改实时数据的副作用.

这可以接受吗?我有什么替代品?我看不到模拟测试的Request对象的方法.即使我这样做,也意味着为Google的API接受的每个可能的代码路径设置结果/入口点(在这种情况下必须通过反复试验找到),但是允许我使用灯具.

进一步扩展是指某些请求依赖于已经存在的某些数据.再次使用Google Content API作为示例,要将数据Feed添加到子帐户,子帐户必须已存在.

我能想到的一种方法是以下步骤;

  1. testCreateAccount
    1. 创建子帐户
    2. 断言子帐户已创建
    3. 删除子帐户
  2. testCreateDataFeed依赖于testCreateAccount没有任何错误
    1. testCreateDataFeed,创建一个新帐户
    2. 创建数据Feed
    3. 断言数据Feed已创建
    4. 删除数据Feed
    5. 删除子帐户

这就提出了进一步的问题; 如何测试帐户/数据Feed的删除?testCreateDataFeed对我来说很脏 - 如果创建数据源失败怎么办?测试失败,因此子帐户永远不会被删除...我无法在没有创建的情况下测试删除,所以在创建然后删除自己的帐户之前testDeleteAccount,我是否编写了另一个依赖的test()testCreateAccount(因为数据不应该在测试之间共享).

综上所述

  • 如何测试与影响实时数据的外部API的交互?
  • 当它们隐藏在抽象层后面时,如何在Integration测试中模拟/存根对象?
  • 当测试失败并且实时数据处于不一致状态时,我该怎么办?
  • 我是如何在代码中实际完成这一切的?

有关:

hak*_*kre 8

这是对已经给出的一个额外答案:

仔细查看代码,它class GoogleAPIRequest具有硬编码的依赖关系class Request.这可以防止您独立于请求类进行测试,因此您无法模拟请求.

您需要使请求可注入,因此您可以在测试时将其更改为模拟.完成后,没有发送真正的API HTTP请求,实时数据不会更改,您可以更快地测试.

  • 同意100%.这就是为什么我已经修改了代码的设计以允许这样做.然而,尽管如此,"类GoogleAPIRequest"有可能有一个方法`getNewRequest()`,它可以被模拟以返回一个模拟的`Request`对象(作为许多可能的替代方案之一). (2认同)