如何编写用于与外部API交互的集成测试?


76

首先,我的知识是:

单元测试是那些测试一小段代码(大多数是单一方法)的测试。

集成测试是测试多个代码区域之间的交互的测试(希望它们已经具有自己的单元测试)。有时,被测代码的某些部分需要其他代码以特定方式起作用。这是Mocks&Stubs出现的地方。因此,我们模拟/存根代码的一部分以非常具体地执行。这使我们的集成测试可以预期地运行而没有副作用。

所有测试都应能够独立运行而无需数据共享。如果必须进行数据共享,则表明系统耦合不够充分。

接下来,我面临的情况是:

与外部API(特别是RESTful API,将通过POST请求修改实时数据)进行交互时,我了解我们可以(应该吗?)模拟出与该API的交互(在此答案中更雄辩地说),以进行集成测试。我也了解我们可以对与该API交互的各个组件进行单元测试(构造请求,解析结果,抛出错误等)。我不明白的是如何真正做到这一点。

所以,最后:我的问题。

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

谷歌的Content API for shopping是一个很好的例子。为了能够执行手头的任务,需要大量的准备工作,然后执行实际的请求,然后分析返回值。其中一些没有任何“沙盒”环境

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

<?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());
    }
}
?>

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

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

现在,此实时请求(希望一切顺利)将执行POST请求,而该请求具有更改实时数据的副作用。

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

进一步的扩展是某些请求依赖于某些已经实时存在的数据时。再次以Google Content API为例,要将数据Feed添加到子帐户,该子帐户必须已经存在。

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

  1. testCreateAccount
    1. 创建一个子账户
    2. 断言子帐户已创建
    3. 删除子账户
  2. 有无testCreateDataFeed是否取决于testCreateAccount没有任何错误
    1. 在中testCreateDataFeed,创建一个新帐户
    2. 创建数据Feed
    3. 断言数据供稿已创建
    4. 删除数据文件
    5. 删除子账户

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

综上所述

  • 如何测试与影响实时数据的外部API的交互?
  • 当对象隐藏在抽象层后面时,如何在集成测试中模拟/存根对象?
  • 如果测试失败并且实时数据处于不一致状态,该怎么办?
  • 我实际上如何在代码中进行所有这些操作?

有关:


那是几个广泛的问题,而不是一个具体的问题。
Raedwald

Answers:


9

这是对已经给出的答案的另一种回答:

查看您的代码,其class GoogleAPIRequest具有的硬编码依赖性class Request。这样可以防止您独立于请求类进行测试,因此无法模拟请求。

您需要使请求可注入,因此您可以在测试时将其更改为模拟。这样,就不会发送真正的API HTTP请求,实时数据也不会改变,您可以更快地进行测试。


2
同意100%。因此,我修改了代码的设计以允许这样做。话虽这么说,可能class GoogleAPIRequest有一种方法getNewRequest()可以被模拟以返回模拟Request对象(作为许多可能的替代方法之一)。
杰西·特尔福德

1

我最近不得不更新一个库,因为它所连接的api已更新。

我的知识不足以详细解释,但是我从查看代码中学到了很多。https://github.com/gridiron-guru/FantasyDataAPI

您可以像往常一样向api提交请求,然后将该响应另存为json文件,然后可以将其用作模拟。

看一下该库中的测试,该库使用Guzzle连接到api。

它模拟了来自api的响应,文档中有大量有关测试如何工作的信息,它可能使您了解如何进行测试。

但基本上,您会手动调用api以及所需的任何参数,并将响应保存为json文件。

当您为api调用编写测试,发送相同的参数并将其加载到模拟中而不是使用实时api时,可以在创建的模拟中测试包含期望值的数据。

有关该API的更新版本,请参见此处。 更新的回购


0

如您所述,测试外部API的方法之一就是创建一个模拟并使用您已经理解的硬编码行为来对其进行处理。

有时,人们将这种测试称为“基于合同的”测试,您可以在其中基于观察和编码的行为针对API编写测试,并且当这些测试开始失败时,“合同就被打破”。如果它们是使用虚假数据的基于REST的简单测试,您还可以将其提供给外部提供程序以运行,以便他们可以发现他们何时/何地可能在更改API时将其更改为新版本,或者发出关于不向后退的警告。兼容。

参考:https : //www.thoughtworks.com/radar/techniques/consumer-driven-contract-testing


0

建立在高票通过的答案之上……

  1. 创建了一个模拟curl对象
  2. 告诉模拟它会期望什么参数
  3. 模拟出函数中curl调用的响应是什么
  4. 让您的代码做到这一点

    $curlMock = $this->getMockBuilder('\Curl\Curl')
                     ->setMethods(['get'])
                     ->getMock();
    
    $curlMock
        ->expects($this->once())
        ->method('get')
        ->with($URL .  '/users/' . urlencode($userId));
    
    $rawResponse = <<<EOL
    {
         "success": true,
         "result": {
         ....
         }
    }
    EOL;
    
    $curlMock->rawResponse = $rawResponse;
    $curlMock->error = null;
    
    $apiService->curl = $curlMock;
    
    // call the function that inherently consumes the API via curl
    $result = $apiService->getUser($userId);
    
    $this->assertTrue($result);
    
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.