【问题标题】:How are integration tests written for interacting with external API?如何编写与外部 API 交互的集成测试?
【发布时间】:2011-09-27 03:55:03
【问题描述】:

首先,我的知识在哪里:

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

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

所有测试都应该能够独立运行而无需数据共享。如果需要数据共享,则表明系统解耦不够。

接下来,我面临的情况:

当与外部 API(特别是 RESTful API 将通过 POST 请求修改实时数据)进行交互时,我知道我们可以(应该?)模拟与该 API 的交互(在this answer 中有更雄辩的说明)用于集成测试。我也知道我们可以对与该 API 交互的各个组件进行单元测试(构造请求、解析结果、抛出错误等)。我不知道如何实际去做。

所以,最后:我的问题。

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

一个完美的例子是Google's Content API for shopping。为了能够执行手头的任务,它需要大量的准备工作,然后执行实际请求,然后分析返回值。其中一些是without any 'sandbox' environment

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

<?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 请求,它具有改变实时数据的副作用。

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

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

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

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

这就引出了另一个问题;如何测试帐户/数据馈送的删除? testCreateDataFeed 让我觉得很脏 - 如果创建数据馈送失败怎么办?测试失败,因此子帐户永远不会被删除...我无法在不创建的情况下测试删除,所以我要编写另一个依赖于testCreateAccount 的测试(testDeleteAccount),然后再创建然后删除自己的帐户(因为数据不应该在测试之间共享)。

总结

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

相关:

【问题讨论】:

标签: unit-testing phpunit integration-testing rest


【解决方案1】:

这更像是对one already given 的附加回答:

查看您的代码,class GoogleAPIRequest 具有硬编码的依赖关系 class Request。这会阻止您独立于请求类对其进行测试,因此您无法模拟请求。

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

【讨论】:

  • 同意 100%。这就是为什么我已经修改了代码的设计以允许这样做。不过话虽如此,class GoogleAPIRequest 有可能有一个方法 getNewRequest() 可以模拟返回一个模拟的 Request 对象(作为许多可能的替代方案之一)。
【解决方案2】:

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

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

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

看看这个库中的测试,它使用 Guzzle 连接到一个 api。

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

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

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

可以在此处找到我的相关 API 的更新版本。 Updated Repo

【讨论】:

    【解决方案3】:

    正如您所提到的,测试外部 API 的一种方法是创建一个模拟并使用您所理解的硬编码行为来处理模拟。

    有时人们将这种类型的测试称为“基于合同”的测试,您可以在其中根据您观察和编码的行为针对 API 编写测试,当这些测试开始失败时,“合同已被破坏” .如果它们是使用虚拟数据的基于 REST 的简单测试,您还可以将它们提供给外部提供程序以运行,以便他们可以发现他们可能在何处/何时更改 API,以至于它应该是一个新版本,或者产生关于不倒退的警告兼容。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-03-20
      • 2014-04-13
      • 2020-06-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多