一、API自动化测试介绍
API 自动化测试
我们已经完成了 Larabbs 所有的接口开发,接下来我们需要将接口交付给其他的工程师对接。将接口部署到生产环境时,如何确保交付的接口正确稳定呢?后续我们还会为项目新增功能,到那时,我们如何保证代码升级过后,接口依然正常?
在开发过程中,我们使用 PostMan 手动一个个接口测试,可当我们有几十个甚至上百个接口时,要同时测试这些接口,手动测试将无法适用。解决方案是自动化测试,自动化测试是保证项目质量的重要环节,这一节我们来了解一下测试的相关概念。
单元测试
单元测试是指对软件中的最小可测试单元进行检查和验证,对于 PHP 来说通常情况下是对某个类中的 某个方法,或者单独的某个方法进行测试。单元测试的目的是首先保证一个系统的 基本组成单元 能正常工作,所有基础零件工作正常了,组装出来的软件才不会出问题。
单元测试是 代码级别 的测试,开发和维护成本都很高,不建议小团队使用。如果是多人协作,我的任务是单独的封装一些通用功能,譬如写个 Service;或者编写一个扩展包,提供一些底层的代码接口,譬如为某个第三方应用封装 SDK,那么单元测试非常有必要。
单元测试涉及的知识点很多,本教程中,我们不对单元测试进行单独讲解。
API 集成测试
Laravel 框架自带的集成 API 测试,我们初始化完整的应用程序上下文,准备好数据库中的测试数据后,就可以方便的模拟各种请求方式,调用接口获取响应结果,最终 断言 返回的结果是否等于预期结果。
API 集成测试 需要准备测试数据,需要设置用户,分配用户权限,为测试接口准备好相关联的数据。虽然有一定得维护成本,但是相比单元测试,维护成本要少很多,在一定程度上更能够保证项目的健壮性。
PostMan 测试
使用 PostMan 等工具进行手动测试,这种是最推崇的测试方案,非常适合小团队使用,因为可以比较真实地模拟用户请求,并且团队协作中,后端工程师可以将 PostMan 的调试接口分享给客户端工程师,接口是否有问题客户端工程师可以自行测试。
二、Laravel API 集成测试
新建文件夹 到要新建文件夹的父目录中,$mkdir 文件夹名称
这一节我们通过几个例子来学习 API 集成测试。
如果你正在使用或者升级到了 Laravel 5.7,那么 phpunit 的版本应该是
7.*
PHPUnit
PHPUnit 是一个轻量级的 PHP 测试框架,Laravel 默认就支持用 PHPUnit 来做测试,并为你的应用程序配置好了 phpunit.xml 文件,只需在命令行上运行 phpunit
就可以进行测试。
现在的 Larabbs 项目中有个小错误,会导致运行 phpunit 的时候报错 PHP Fatal error: Cannot redeclare route_class()
。只是因为我们的自定义方法的引入有些问题,稍微修改一下:
bootstrap/app.php
.
.
.
require_once __DIR__ . '/helpers.php';
.
.
.
之前的代码中使用的是 require
引入自定义方法,可能会重复引入,所以这里改为 require_once
。
尝试在 larabbs 根目录执行 phpunit
$ phpunit
创建测试文件
首先需要创建一个测试文件:
$ php artisan make:test TopicApiTest
该命令会在 tests/Feature
目录中创建 TopicApiTest.php
文件,我们会发现 tests 目录中有 Feature
和 Unit
两个目录,如何区分这两个目录呢?
- Unit —— 单元测试是从程序员的角度编写的。它们用于确保类的特定方法执行一组特定任务。
- Feature —— 功能测试是从用户的角度编写的。它们确保系统按照用户期望的那样运行,包括几个对象的相互作用,甚至是一个完整的 HTTP 请求。
我们测试 API 属于功能测试,应该创建在 Feature
目录中。
测试发布话题
tests/Feature/TopicApiTest.php
<?php
namespace Tests\Feature;
use App\Models\User;
use App\Models\Topic;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
class TopicApiTest extends TestCase
{
protected $user;
public function setUp()
{
parent::setUp();
$this->user = factory(User::class)->create();
}
public function testStoreTopic()
{
$data = ['category_id' => 1, 'body' => 'test body', 'title' => 'test title'];
$token = \Auth::guard('api')->fromUser($this->user);
$response = $this->withHeaders(['Authorization' => 'Bearer '.$token])
->json('POST', '/api/topics', $data);
$assertData = [
'category_id' => 1,
'user_id' => $this->user->id,
'title' => 'test title',
'body' => clean('test body', 'user_topic_body'),
];
$response->assertStatus(201)
->assertJsonFragment($assertData);
}
}
setUp
方法会在测试开始之前执行,我们先创建一个用户,测试会以该用户的身份进行测试。
testStoreTopic
就是一个测试用户,测试发布话题。使用 $this->json 可以方便的模拟各种 HTTP 请求:
- 第一个参数 —— 请求的方法,发布话题使用的是 POST 方法;
- 第二个参数 —— 请求地址,请求
/api/topics
; - 第三个参数 —— 请求参数,传入
category_id
,body
,title
,这三个必填参数; - 第四个参数 —— 请求 Header,可以直接设置
header
,也可以利用withHeaders
方法达到同样的目的;
我们发现为用户生成 Token
以及设置 Authorization
部分的代码,不仅 修改话题
,删除话题
会使用,以后编写的其他功能的测试用例一样会使用,所以我们进行一下封装。
增加一个 Trait:
$ touch tests/Traits/ActingJWTUser.php
tests/Traits/ActingJWTUser.php
<?php
namespace Tests\Traits;
use App\Models\User;
trait ActingJWTUser
{
public function JWTActingAs(User $user)
{
$token = \Auth::guard('api')->fromUser($user);
$this->withHeaders(['Authorization' => 'Bearer '.$token]);
return $this;
}
}
修改测试用例,使用该 Trait。
tests/Feature/TopicApiTest.php
<?php
namespace Tests\Feature;
.
.
.
use Tests\Traits\ActingJWTUser;
.
.
.
class TopicApiTest extends TestCase
{
use ActingJWTUser;
.
.
.
public function testStoreTopic()
{
$data = ['category_id' => 1, 'body' => 'test body', 'title' => 'test title'];
$response = $this->JWTActingAs($this->user)
->json('POST', '/api/topics', $data);
$assertData = [
'category_id' => 1,
'user_id' => $this->user->id,
'title' => 'test title',
'body' => clean('test body', 'user_topic_body'),
];
$response->assertStatus(201)
->assertJsonFragment($assertData);
}
}
这样我们就能方便的使用 JWTActingAs
方法,登录一个用户。最后得到的响应 $response
,通过 assertStatus
断言响应结果为 201
,通过 assertJsonFragment
断言响应结果包含 assertData
数据。
执行测试:
$ phpunit
卡在此处一直报错:Error: Call to undefined method Illuminate\Auth\TokenGuard::fromUser()
等待解决
测试修改话题
tests/Feature/TopicApiTest.php
.
.
.
public function testUpdateTopic()
{
$topic = $this->makeTopic();
$editData = ['category_id' => 2, 'body' => 'edit body', 'title' => 'edit title'];
$response = $this->JWTActingAs($this->user)
->json('PATCH', '/api/topics/'.$topic->id, $editData);
$assertData= [
'category_id' => 2,
'user_id' => $this->user->id,
'title' => 'edit title',
'body' => clean('edit body', 'user_topic_body'),
];
$response->assertStatus(200)
->assertJsonFragment($assertData);
}
protected function makeTopic()
{
return factory(Topic::class)->create([
'user_id' => $this->user->id,
'category_id' => 1,
]);
}
.
.
.
我们增加了 testUpdateTopic
测试用例,要修改话题,首先需要为用户创建一个话题,所以增加了 makeTopic
,为当前测试的用户生成一个话题。代码与发布话题类似,准备好要修改的话题数据 $editData
,调用 修改话题
接口,修改刚才创建的话题,最后断言响应状态码为 200
以及结果中包含 $assertData
。
执行测试:
$ phpunit
测试查看话题
tests/Feature/TopicApiTest.php
.
.
.
public function testShowTopic()
{
$topic = $this->makeTopic();
$response = $this->json('GET', '/api/topics/'.$topic->id);
$assertData= [
'category_id' => $topic->category_id,
'user_id' => $topic->user_id,
'title' => $topic->title,
'body' => $topic->body,
];
$response->assertStatus(200)
->assertJsonFragment($assertData);
}
public function testIndexTopic()
{
$response = $this->json('GET', '/api/topics');
$response->assertStatus(200)
->assertJsonStructure(['data', 'meta']);
}
.
.
.
增加了两个测试用户 testShowTopic
和 testIndexTopic
,分别测试 话题详情
和 话题列表
。这两个接口不需要用户登录即可访问,所以不需要传入 Token。
testShowTopic
先创建一个话题,然后访问 话题详情
接口,断言响应状态码为 200
以及响应数据与刚才创建的话题数据一致。
testIndexTopic
直接访问 话题列表
接口,断言响应状态码为 200
,断言响应数据结构中有 data
和 meta
。
执行测试:
$ phpunit
测试删除话题
tests/Feature/TopicApiTest.php
.
.
.
public function testDeleteTopic()
{
$topic = $this->makeTopic();
$response = $this->JWTActingAs($this->user)
->json('DELETE', '/api/topics/'.$topic->id);
$response->assertStatus(204);
$response = $this->json('GET', '/api/topics/'.$topic->id);
$response->assertStatus(404);
}
.
.
.
首先通过 makeTopic
创建一个话题,然后通过 DELETE
方法调用 删除话题
接口,将话题删除,断言响应状态码为 204
。
接着请求话题详情接口,断言响应状态码为 404
,因为该话题已经被删除了,所以会得到 404
。
执行测试:
$ phpunit
最终我们执行了 7 个测试用户,进行了 22 次断言,测试正确。
代码版本控制
$ git add -A
$ git commit -m 'topic test'
三、第三方黑盒测试
除了单元测试以及集成测试之外,还可以利用第三方工具,这一节我们将学习如何利用 PostMan 进行第三方黑盒测试,这也是我们最推崇的测试方案。
第三方黑盒测试的好处是可以最大程度测试 整套系统,我们的 API 接口,从 PHP 代码解析开始,以下涉及因素都会影响到接口的可用性:
- 软件代码级别的错误;
- 程序使用的第三方软件发生错误,如:Redis 缓存和队列系统、MySQL 数据库等;
- API 服务器上的系统软件,如 Nginx、Cron 等;
- API 服务器上的物理问题,如硬盘坏了;
- 域名解析问题,如 DNS 解析出错;
代码级别的自动化测试,能测试的范围有限。而第三方黑盒测试,模拟的是真实用户的请求,将 API 服务器看成 完全的系统,系统里任何一个部件坏了,都能被检测出来。并且这种测试方法与服务器端环境彻底解耦,后期维护成本较低。
分享接口数据
PostMan 支持我们导出保存的接口,在团队协作中,后端工程师可以方便的将 PostMan 的接口数据分享给客户端工程师,客户端工程师可以自行测试接口,真实的模拟请求。
导出 Collection
有多种导出格式可选,我们选择 PostMan 推荐的 Collection v2.1
。
选择导出后,就得到了 Larabbs
的接口文件,文件名类似 Larabbs.postman_collection.json
。
导出环境变量
除了接口数据外,我们还定义了一些环境变量,例如 {{host}}
,{{jwt_user1}}
等,我们也需要将其导出,分享给他人,这样才是一个完整的环境。
点击右上角的设置,选择 Manage Environments
。
点击对应环境后面的下载即可。
下载的文件名类似 larabbs-local.postman_environment.json
。
这里要注意我们现在的环境 larabbs-local
是我们本地的环境,为了方便客户端工程师使用,可以在某个线上可访问的测试服务器搭建完整的 Larabbs 环境,增加测试服务器的环境 larabbs-test
,设置测试环境的环境变量 {{host}}
,{{jwt_user1}}
等,这样分享出去的环境,别人可以直接使用。
导入 Collection 及环境
我们将导出的 Larabbs.postman_collection.json
和 larabbs-local.postman_environment.json
两个文件分享给客户端的工程师。
点击左上角的 Import
即可导入 Collection 文件。
在 Manage Environments
中点击 Import
即可导入环境变量。
导入成功后,我们就可以直接对接口进行真实调试。
PostMan 自动化测试
PostMan 为我们提供了自动化测试的功能,类似于 Laravel 的接口测试,PostMan 可以请求接口,并且断言响应结果和响应数据,接下来我们以 发布话题
和 话题列表
两个接口为例,进行自动化测试。
测试发布话题
打开 发布话题
接口,可以看到有个 Tests
的选项卡,点击该选项卡会出现一个空白区域,在这里我们可以添加一些断言,判断请求结果。
填入如下内容:
pm.test("响应状态码正确", function () {
pm.response.to.have.status(201);
});
pm.test("接口响应数据正确", function () {
pm.expect(pm.response.text()).to.include("id");
pm.expect(pm.response.text()).to.include("title");
pm.expect(pm.response.text()).to.include("body");
pm.expect(pm.response.text()).to.include("user_id");
pm.expect(pm.response.text()).to.include("category_id");
});
PostMan 为我们提供了 pm.test
方法,相当于一个测试用例,第一个参数是执行正确后的提示文字,第二个参数是个闭包,执行我们的断言。
第一个测试用户我们判断响应的状态码,pm.response.to.have.status(201);
断言响应结果的状态码是 201
。
第二个测试用户,我们判断响应数据,通过 pm.expect(pm.response.text()).to.include("");
断言响应数据中一定会包含某个字段。
点击 Send
进行调试。
切换到 Test Results
,可以看到两个测试用例均通过了。
测试话题列表
同样的,我们为 话题列表
接口增加测试用例:
// example using response assertions
pm.test("响应状态码正确", function () {
pm.response.to.have.status(200);
});
pm.test("接口响应数据正确", function () {
pm.expect(pm.response.text()).to.include("data");
pm.expect(pm.response.text()).to.include("meta");
});
同样增加了两个测试用例,断言响应状态码为 200
,断言响应数据中包含 data
和 meta
。
点击 Send
进行调试。
测试通过。
批量测试
每个接口都完成测试用户后,我们就可以通过 PostMan 的测试工具进行自动化测试了。
点击 PostMan 左上角的 Runner
,我们可以看到 PostMan 的自动化测试界面,我们可以选择测试整个项目,或者测试某个目录。这里我们选择 话题
目录,选择 larabbs-local
环境,执行测试。
我们可以看到 PostMan 依次请求了 话题
目录下的所有接口,因为我们为 发布话题
和 话题列表
添加了测试用例和断言,所以看到这两个接口的测试用户均已通过。
我们可以为所有的接口增加测试用例,这样当接口升级之后,可以方便的通过 PostMan 的自动化测试工具进行测试,快速定位不符合预期的接口。
四、API文档
完成了所有的 API 及测试,我们需要有个一份接口文档,方便他人使用,这一节我们来介绍一下快速生成 API 文档的方法。
PostMan
PostMan 导出的 Collection 其实已经是一份基础的接口文档,可以进一步补充请求参数的描述信息。
当然也有很多缺点:
- 需要通过导入导出来分享文档;
- 没法对响应结果进行详细说明;
- 没有地方添加关于当个接口的进一步说明;
当然了付费用户可以享受更多方便的功能,我们在这里不做进一步讨论。
Apizza 介绍
Apizza (apizza - 极客专属的api管理工具)是一款在线的 API 协作管理工具,界面及使用方式与 PostMan 基本类似,可以理解为一款在线版的 PostMan。
但是与 PostMan 相比,功能要更加的丰富,比如我们可以更加详细的定义请求参数和参数类型。
可以更加详细的描述响应数据。
支持 Markdown 格式的说明文件,我们可以为一组接口增加详细的调用说明。
另外 Apizza 还支持直接导入 PostMan 的 Collection 文件。
注意目前只支持 PostMan v1 版本的 Collection 文件。从 PostMan 导出一份 v1 版本的文件,导入 Apizza 中。
导出成功后即可看到文档已经同 PostMan 保持一致了。
当然还有团队协作以及文档分享功能。
总结
开发阶段我们一定会使用 PostMan 进行接口调试,将 PostMan 的 Collection 文件导入 Apizza,然后再与团队成员一起进一步的完善文档,最后分享给他人使用,是一件方便快速的事情。
虽然 APIdoc 和 swagger 都是十分优秀的文档工具,但是依然有一定的学习成本和维护成本。所以我们更加推荐 PostMan 与 Apizza 这样的在线工具结合使用,快速的完成 API 文档。