Stub-API 下的接口自动测试
前言
StubApi是一套集接口管理,文档管理,数据字典,自动测试等功能于一体的接口工具。
StubApi力求能使开发人员自己解决接口自动测试需求
,在单元测试过于繁琐
和测试覆盖率不理想
等问题上寻找能够接受的平衡点
,最终使中小团队可以使用自动测试提升开发效率并保障系统稳定
。
使用方式
使用条件
自动测试功能实现依赖于StubApi的接口类型和接口格式,所以目前只能使用在StubApi接口上。
执行流程
在完成接口开发后,自动测试操作分为三部分
- 构建基境
- 填写测试用例
- 生成期望结果
完成以上三步操作,在需要测试时只需要执行phpunit即可测试接口。
构建基境(setUp)
将整个场景设置成某个已知的状态,并在测试结束后将其复原到初始状态。这个已知的状态称为测试的 基境(fixture)。
基境是StubApi测试时接口的操作数据,初始基境可以通过以下几种方式实现:
- 使用Laravel 的 seeder 插入数据库。这种方式一般在项目或功能开始开发阶段使用,这时期测试库数据积累较少,数据格式频繁变动。使用seeder能够快速相应结构变动,但需要更多人力去设计需要被测试的数据。这种方式另一个缺点是性能较差,因为开发人员很可能为了可维护性使用Model创建数据,这样做的性能是远远不及sql和\DB创建数据的。
- 使用导出的数据文件重建数据库。在项目或功能成熟后更建议使用这种方式重建数据库,因为这些数据的“仿真度”更高,填充数据库的效率更高。
防止id自增
一般情况下我们只需要在基境被破坏的情况下重新创建,stub-api执行时可以通过一些方式避免自增id扰乱测试,所以不需要每次都重建数据。
在可能自增或随机变动的字段上加@Mutable,标识字段在自动测试中忽略,所有该字段的相应都会变成字符串*,也就不会产生diff。
/** 事件 / 对应描述 / 也包含模板信息 */
class NotificationEvent extends NotificationEventEditPayload {
// 对应枚举
@Mutable
Integer id;
// 推送规则实体
@Optional
@DBField("rule_entity")
Models.TemplatePriorityRule ruleEnity;
// 免打扰实体
@Optional
@DBField("silent_delay_entity")
Models.NotificationSilentDelay silentDelayEntity;
/** 组内的模板 */
@Optional
Models.MessageTemplate[] templates;
}
编写测试用例
Stub api的测试只需要填写接口输入的数据,所有测试用例都存放在resources/generator/testcases下。输入的json格式可以直接使用字符串,也可以用yaml的对象格式,添加数组的方式也是同样的。yaml的一些语法,如引用,都是可以正常使用的。
如果没有自动生成yaml文件,请修改resources/generator/config/application.yml文件
将testcases放入generator.process.default中
generator:
process:
default: laravel,laravel-doc,postman,testcases
laravel-auto-test: laravel-auto-test
client: nodejs-client
生成测试用例文件后,首先我们需要把__enabled改为true,在false时会跳过这个测试用例。
参数填写在params中,如果需要多个测试用例,可以在testcases中以数组的形式输入。
下面是个典型例子:
# __api: /api/api/v1/message/add_template_to_event
__enabled: true
__role:
__user:
__anchor:
__params:
__testcases:
-
__params:
eventId: 1
payload:
accountId: 1
status: 1
channel: 2
templateContent:
qqMiniProgramTemplateMessage:
officialAccountTemplateMessage:
officialAccountCustomMessage:
miniProgramTemplateMessage: {"touser":"slfjdsl","templateId":"fjdslfjdslfjsd","page":"sdfjl?jfdsk","formId":null,"data":{"keyword1":{"label":"keyword1","value":"sdfdsfdsfds"},"keyword2":{"label":"keyword2","value":"sdfdsfdsfds2"},"keyword3":{"label":"keyword3","value":"sdfdsfdsfds3"}},"emphasisKeyword":"keyword1.DATA"}
systemMessage:
-
__params:
eventId: 1
payload:
accountId: 1
status: 1
channel: 2
templateContent:
placeholder: placeholder
-
__params:
eventId: 1
payload:
accountId: 2
status: 1
channel: 1
templateContent:
qqMiniProgramTemplateMessage:
officialAccountTemplateMessage:
officialAccountCustomMessage:
text:
-
content: lsfjdslkfsl
url: http://baidu.com
miniProgramTemplateMessage: {"touser":"slfjdsl","templateId":"fjdslfjdslfjsd","page":"sdfjl?jfdsk","formId":null,"data":{"keyword1":{"label":"keyword1","value":"sdfdsfdsfds"},"keyword2":{"label":"keyword2","value":"sdfdsfdsfds2"},"keyword3":{"label":"keyword3","value":"sdfdsfdsfds3"}},"emphasisKeyword":"keyword1.DATA"}
systemMessage:
生成期望结果
在bin目录下执行autoTest,将会读取测试用例目录下的所有测试用例,并用这些测试用例依次请求接口获得期望结果并生成phpunit文件。
这里我就把整个文件内容放进来了,包含testcase 0 - 1 - 2 共三个测试用例,如上边我们输入的。
<?php
namespace Tests\Generated\V1\Message;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\TestCase;
class AddTemplateToEventTest extends TestCase
{
use DatabaseTransactions;
public function testCase0()
{
$response = $this->post('/api/v1/message/add_template_to_event', [
'__role' => '',
'__user' => '',
'eventId' => '1',
'payload' => '
{
"accountId": 1,
"status": 1,
"channel": 2,
"templateContent": {
"miniProgramTemplateMessage": {
"touser": "slfjdsl",
"templateId": "fjdslfjdslfjsd",
"page": "sdfjl?jfdsk",
"data": {
"keyword1": {
"label": "keyword1",
"value": "sdfdsfdsfds"
},
"keyword2": {
"label": "keyword2",
"value": "sdfdsfdsfds2"
},
"keyword3": {
"label": "keyword3",
"value": "sdfdsfdsfds3"
}
},
"emphasisKeyword": "keyword1.DATA"
}
}
}
',
]);
$actual = $response->getContent();
$expect = '
{
"status": 0,
"message": "message",
"data": {
"template": {
"accountId": 1,
"channel": 2,
"templateContent": {
"officialAccountTemplateMessage": null,
"officialAccountCustomMessage": null,
"miniProgramTemplateMessage": {
"label": null,
"page": "sdfjl?jfdsk",
"templateId": "fjdslfjdslfjsd",
"emphasisKeyword": "keyword1.DATA",
"formId": null,
"data": {
"keyword1": {
"label": "keyword1",
"value": "sdfdsfdsfds"
},
"keyword2": {
"label": "keyword2",
"value": "sdfdsfdsfds2"
},
"keyword3": {
"label": "keyword3",
"value": "sdfdsfdsfds3"
},
"keyword4": null
}
},
"qqMiniProgramTemplateMessage": null,
"systemMessage": null
},
"id": "*",
"eventId": 1,
"status": 0
}
}
}
';
$expect = json_encode(json_decode($expect));
$this->assertJsonStringEqualsJsonString($expect, $actual);
}
public function testCase1()
{
$response = $this->post('/api/v1/message/add_template_to_event', [
'__role' => '',
'__user' => '',
'eventId' => '1',
'payload' => '
{
"accountId": 1,
"status": 1,
"channel": 2,
"templateContent": {
"placeholder": "placeholder"
}
}
',
]);
$actual = $response->getContent();
$expect = '
{
"status": -2,
"message": "未配置对应渠道的模板信息"
}
';
$expect = json_encode(json_decode($expect));
$this->assertJsonStringEqualsJsonString($expect, $actual);
}
public function testCase2()
{
$response = $this->post('/api/v1/message/add_template_to_event', [
'__role' => '',
'__user' => '',
'eventId' => '1',
'payload' => '
{
"accountId": 2,
"status": 1,
"channel": 1,
"templateContent": {
"officialAccountCustomMessage": {
"text": [
{
"content": "lsfjdslkfsl",
"url": "http://baidu.com"
}
]
},
"miniProgramTemplateMessage": {
"touser": "slfjdsl",
"templateId": "fjdslfjdslfjsd",
"page": "sdfjl?jfdsk",
"data": {
"keyword1": {
"label": "keyword1",
"value": "sdfdsfdsfds"
},
"keyword2": {
"label": "keyword2",
"value": "sdfdsfdsfds2"
},
"keyword3": {
"label": "keyword3",
"value": "sdfdsfdsfds3"
}
},
"emphasisKeyword": "keyword1.DATA"
}
}
}
',
]);
$actual = $response->getContent();
$expect = '
{
"status": 0,
"message": "message",
"data": {
"template": {
"accountId": 2,
"channel": 1,
"templateContent": {
"officialAccountTemplateMessage": null,
"officialAccountCustomMessage": {
"text": [
{
"content": "lsfjdslkfsl",
"appId": null,
"path": null,
"url": "http:\/\/baidu.com"
}
],
"image": null,
"voice": null,
"mpnews": null,
"news": null
},
"miniProgramTemplateMessage": {
"label": null,
"page": "sdfjl?jfdsk",
"templateId": "fjdslfjdslfjsd",
"emphasisKeyword": "keyword1.DATA",
"formId": null,
"data": {
"keyword1": {
"label": "keyword1",
"value": "sdfdsfdsfds"
},
"keyword2": {
"label": "keyword2",
"value": "sdfdsfdsfds2"
},
"keyword3": {
"label": "keyword3",
"value": "sdfdsfdsfds3"
},
"keyword4": null
}
},
"qqMiniProgramTemplateMessage": null,
"systemMessage": null
},
"id": "*",
"eventId": 1,
"status": 0
}
}
}
';
$expect = json_encode(json_decode($expect));
$this->assertJsonStringEqualsJsonString($expect, $actual);
}
}
校验期望结果
假设我们因为需求的变更重写了一部分代码的逻辑,此时我们需要进行自动测试,并通过分析测试结果判断逻辑是否正确,单元测试输入和期望结果是否需要调整。
首先我们需要初始测试基境,这里和setUp的方法相似。
执行vendor/bin/phpunit
不通过的测试用例会显示为F,并在后边把增减的行显示出来。
root@89b83c5748ec:/var/www# vendor/bin/phpunit --coverage-html=./storage/app/phpunit/
PHPUnit 7.5.14 by Sebastian Bergmann and contributors.
.R......FFF..FFF.....................FF.... 43 / 43 (100%)
Time: 1.49 minutes, Memory: 34.00 MB
这里会显示有区别的行-表示期望有但是结果中没有,+表示期望结果中没有这一项,但是却返回了。
@@ @@
"id": "*",
"access": null,
"template": null,
- "requestCombined": {
- "officialAccountTemplateMessage": null,
- "officialAccountCustomMessage": null,
- "miniProgramTemplateMessage": null,
- "qqMiniProgramTemplateMessage": null,
- "systemMessage": null
- },
+ "request": "",
"response": "",
"uuid": "*"
},
@@ @@
用phpunit的覆盖率工具显示覆盖率
使用方式不再赘述,这里我们用vendor/bin/phpunit --coverage-html=./storage/app/phpunit/演示
测试覆盖率不到100的原因有很多,比如测试集较少(没有覆盖边缘参数,导致判断边缘参数的代码没有覆盖),一些判断production后执行的代码没有覆盖(if (config('app.env') === 'production'))。
覆盖率高低并没有好坏之分,是基于成本的取舍。即使100覆盖也不能说明测试是完备的,abcdef六种选二逻辑组合,只需要三组就能达到纸面上的100覆盖,盲目强调覆盖可能得到自欺欺人的结果。
与单元测试比较
相对于单元测试的流程
构建基境 -> 设计编写单元测试代码 -> 验证测试结果
Stub api的测试省略了编写单元测试代码的步骤,转而直接使用接口进行测试。这样做测试显然没有单元测试的可定制性强,但单元测试有高昂但学习成本和代码量,甚至写单元测试的断言的工作量就远远超过接口测试。Stub api的测试仅需要设置请求参数,如同开发自己调试代码。
本作品采用《CC 协议》,转载必须注明作者和本文链接