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的測試僅需要設定請求引數,如同開發自己除錯程式碼。