单元测试是测试一小段代码的方法(主要是单一方法)。
集成测试是那些测试多个代码区域之间交互的测试(希望它们已经有了自己的单元测试)。有时,部分测试代码需要其他代码以特定方式执行操作。这是Mocks&存根进来。因此,我们模拟/存根一部分代码来执行非常具体的操作。这使我们的集成测试可以预测地运行而没有副作用。
所有测试都应该能够在没有数据共享的情况下独立运行。如果需要数据共享,这表明系统没有足够的分离。
当与外部API(特别是将使用POST请求修改实时数据的RESTful API)进行交互时,我知道我们可以(应该?)模拟与该API的交互(更详尽地在{{3}中说明) })用于集成测试。我也理解我们可以单元测试与该API交互的各个组件(构造请求,解析结果,抛出错误等)。我没有得到的是如何实际解决这个问题。
如何测试与具有副作用的外部API的交互?
一个完美的例子是this answer。为了能够执行手头的任务,它需要大量的准备工作,然后执行实际请求,然后分析返回值。其中一些是Google's 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接受的每个可能的代码路径设置结果/入口点(在这种情况下必须通过反复试验找到),但是允许我使用灯具。 / p>
进一步扩展是指某些请求依赖于已经存在的某些数据。再次使用Google内容API作为示例,要将数据Feed添加到子帐户,子帐户必须已存在。
我能想到的一种方法是以下步骤;
testCreateAccount
testCreateDataFeed
依赖testCreateAccount
没有任何错误
testCreateDataFeed
中,创建一个新帐户然后提出了进一步的问题;如何测试帐户/数据Feed的删除? testCreateDataFeed
对我来说很脏 - 如果创建数据Feed失败怎么办?测试失败,因此永远不会删除子帐户...我无法在没有创建的情况下测试删除,因此在创建然后删除之前,我是否编写了依赖testDeleteAccount
的其他测试(testCreateAccount
)自己的帐户(因为不应该在测试之间共享数据)。
相关:
答案 0 :(得分:8)
这是one already given的另一个答案:
通过查看代码,class GoogleAPIRequest
具有class Request
的硬编码依赖关系。这可以防止您独立于请求类进行测试,因此您无法模拟请求。
您需要使请求可注入,因此您可以在测试时将其更改为模拟。完成后,不会发送真正的API HTTP请求,实时数据不会更改,您可以更快地测试。
答案 1 :(得分:1)
我最近不得不更新库,因为它连接的api已更新。
我的知识还不足以详细解释,但我从查看代码中学到了很多东西。 https://github.com/gridiron-guru/FantasyDataAPI
您可以像往常一样向api提交请求,然后将该响应保存为json文件,然后您可以将其用作模拟。
查看此库中使用Guzzle连接到api的测试。
它嘲笑来自api的回复,关于测试如何工作的文档中有大量信息,它可能会让你知道如何去做。
但基本上你手动调用api以及你需要的任何参数,并将响应保存为json文件。
当您为api调用编写测试时,发送相同的参数并使其在模拟中加载而不是使用实时api,然后您可以测试您创建的模拟中的数据包含预期值。 / p>
我可以在此处找到有关api的更新版本。 Updated Repo
答案 2 :(得分:0)
测试外部API的方法之一就像你提到的那样,通过创建一个模拟并使用硬编码的行为来解决它。你可以理解它。
有时人们会将此类测试称为&#34;基于合同的&#34;测试,您可以根据您观察和编码的行为针对API编写测试,并且当这些测试开始失败时,&#34;合同被打破&#34;。如果它们是使用虚拟数据的基于REST的简单测试,您还可以将它们提供给外部提供程序以便运行,这样它们就可以发现它们可能在何时/何时更改API应该是新版本,或者发出关于不向后的警告兼容。
参考:https://www.thoughtworks.com/radar/techniques/consumer-driven-contract-testing
答案 3 :(得分:0)
建立在高投票回答的基础之上......这就是我如何做到并且安静地工作。
让你的代码做到这一点
$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);