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

首先,我的知识是什么:

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

集成测试 是测试多个代码区域之间交互的测试(希望它们已经有了自己的单元测试)。有时,被测代码的某些部分需要其他代码以特定的方式进行操作。这就是模拟与存根的用武之地。因此,我们模拟/存根掉一部分代码,以便非常具体地执行。这使得我们的集成测试可以在没有副作用的情况下可预测地运行。

所有测试都应该能够独立运行,而不需要共享数据。如果数据共享是必要的,这表明系统解耦程度不够。

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

当与外部 API 交互时(特别是将使用 POST 请求修改活动数据的 RESTful API) ,我理解我们可以(应该?)为集成测试模拟与该 API 的交互(在 这个答案中更有说服力地说明)。我还知道我们可以单元测试与 API 交互的各个组件(构造请求、解析结果、抛出错误等)。我不明白的是该怎么做。

最后: 我的问题。

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

用于购物的 Google 内容 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());
}
}
?>

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

假设 testTheRequest是测试套件调用的方法,那么示例将执行一个实时请求。

现在,这个活动请求将(希望一切顺利)执行一个 POST 请求,该请求的副作用是改变活动数据。

这样可以吗?我还有别的选择吗?我找不到为测试模拟 Request 对象的方法。即使我这样做了,这也意味着为 Google 的 API 接受的每个可能的代码路径设置结果/入口点(在这种情况下,必须通过试验和错误才能找到) ,但是允许我使用 fixture。

进一步的扩展是当某些请求依赖于某些已经在 Live 中的数据时。再次使用 GoogleContentAPI 作为示例,要向子帐户添加数据源,子帐户必须已经存在。

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

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

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

摘要

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

相关阅读:

33760 次浏览

This is more an additional answer to the one already given:

Looking through your code, the class GoogleAPIRequest has a hard-encoded dependency of class Request. This prevents you from testing it independently from the request class, so you can't mock the request.

You need to make the request injectable, so you can change it to a mock while testing. That done, no real API HTTP requests are send, the live data is not changed and you can test much quicker.

I've recently had to update a library because the api it connects to was updated.

My knowledge isn't enough to explain in detail, but i learnt a great deal from looking at the code. https://github.com/gridiron-guru/FantasyDataAPI

You can submit a request as you would normally to the api and then save that response as a json file, you can then use that as a mock.

Have a look at the tests in this library which connects to an api using Guzzle.

It mocks responses from the api, there's a good deal of information in the docs on how the testing works it might give you an idea of how to go about it.

but basically you do a manual call to the api along with any parameters you need, and save the response as a json file.

When you write your test for the api call, send along the same parameters and get it to load in the mock rather than using the live api, you can then test the data in the mock you created contains the expected values.

My Updated version of the api in question can be found here. Updated Repo

One of the ways to test out external APIs is as you mentioned, by creating a mock and working against that with the behavior hard coded as you have understood it.

Sometimes people refer to this type of testing as "contract based" testing, where you can write tests against the API based on the behavior you have observed and coded against, and when those tests start failing, the "contract is broken". If they are simple REST based tests using dummy data you can also provide them to the external provider to run so they can discover where/when they might be changing the API enough that it should be a new version or produce a warning about not being backwards compatible.

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