首页 文章

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

提问于
浏览
62

首先,我的知识在哪里:

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

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

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

接下来,我面临的情况:

当与外部API(特别是将使用POST请求修改实时数据的RESTful API)交互时,我理解我们可以(应该?)模拟与该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请求,其具有更改实时数据的副作用 .

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

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

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

  • testCreateAccount

  • 创建子帐户

  • 断言子帐户已创建

  • 删除子帐户

  • testCreateDataFeed 依赖于 testCreateAccount 没有任何错误

  • testCreateDataFeed 中,创建一个新帐户

  • 创建数据Feed

  • 断言数据Feed已创建

  • 删除数据Feed

  • 删除子帐户

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

摘要

  • 如何测试与影响实时数据的外部API的交互?

  • 当它们隐藏在抽象层后面时,如何在集成测试中模拟/存根对象?

  • 如果测试失败并且实时数据处于不一致状态,我该怎么办?

  • 如何在代码中实际执行所有这些操作?


有关:

5 回答

  • 8

    如何测试与影响实时数据的外部API的交互?

    你没有 . 你必须真正相信实际的API实际上是有效的 .

    您可以 - 并且应该 - 使用实时数据来运行API,以确保您理解它 .

    但是你不需要测试它 . 如果API不起作用,只需停止使用它 . 不要测试每个边缘和角落的情况 .

    当它们隐藏在抽象层后面时,如何在Integration测试中模拟/存根对象?

    那个's the point. Test the abstraction. You have to trust that the implementation works. You'重新测试 your 代码 . 不是他们的代码 .

    当测试失败并且实时数据处于不一致状态时,我该怎么办?

    什么?为什么要测试实时API以确保它们有效?你不相信他们?如果你不信任他们,请不要测试 . 找到值得信赖的供应商 .

    您只测试 your 代码 . 你信任 their 代码 . 你琐碎地模拟了他们的代码,以确保你的代码有效 .


    你是怎么做到的

    • 使用API . 发送请求 . 得到回应 .

    • 使用您的应用程序 . 找出你要发送的请求类型 .

    • 返回API . 发送一个已知的好请求 . 得到回应 . Save this response . 这是您对良好要求的黄金标准回应 . 长期委任这成了一个测试案例 .

    • 现在,您可以使用您的应用程序,因为您知道您的真实API提供了金标准响应 . 这应该足以开始处理响应 .

    • 在完成一些用例(好的请求,错误的请求)之后,您应该能够从API获得良好的响应和一些典型的错误响应 . 保存好消息和错误消息 . 这些对于单元测试非常有用,可以确保您正确处理某些类型的响应 .

  • 61

    这是one already given的另一个答案:

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

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

  • 0

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

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

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

    看看这个库中使用Guzzle连接到api的测试 .

    它嘲笑来自api的响应,在文档中有大量关于测试如何工作的信息,它可能会让你知道如何去做 .

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

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

    我可以在这里找到我的更新版本的api . Updated Repo

  • 0

    测试外部API的方法之一就是你提到的,通过创建一个模拟并与你已经理解的硬编码行为相反 .

    有时人们将这种类型的测试称为“基于 Contract ”的测试,您可以根据您观察和编码的行为针对API编写测试,当这些测试开始失败时,“ Contract 就会被破坏” . 如果它们是使用虚拟数据的基于REST的简单测试,您还可以将它们提供给外部提供程序以便运行,这样它们就可以发现它们可能在何时/何时更改API以使其成为新版本或产生关于不向后的警告兼容 .

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

  • 1

    Build 在高投票回答的基础之上......这就是我如何做到这一点并且安静地工作 .

    • 创建了一个模拟卷曲对象

    • 告诉模拟它会期待什么参数

    • 模拟你的函数中curl调用的响应

    • 让你的代码做到这一点

    $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);
    

相关问题