Stub-API 下的介面自動測試

Kamicloud 發表於 2019-08-11

StubApi是一套集介面管理,文件管理,資料字典,自動測試等功能於一體的介面工具。

StubApi力求能使開發人員自己解決介面自動測試需求,在單元測試過於繁瑣測試覆蓋率不理想等問題上尋找能夠接受的平衡點,最終使中小團隊可以使用自動測試提升開發效率並保障系統穩定

使用條件

自動測試功能實現依賴於StubApi的介面型別和介面格式,所以目前只能使用在StubApi介面上。

執行流程

在完成介面開發後,自動測試操作分為三部分

  1. 構建基境
  2. 填寫測試用例
  3. 生成期望結果

完成以上三步操作,在需要測試時只需要執行phpunit即可測試介面。

構建基境(setUp)

將整個場景設定成某個已知的狀態,並在測試結束後將其復原到初始狀態。這個已知的狀態稱為測試的 基境(fixture)

基境是StubApi測試時介面的運算元據,初始基境可以通過以下幾種方式實現:

  1. 使用Laravel 的 seeder 插入資料庫。這種方式一般在專案或功能開始開發階段使用,這時期測試庫資料積累較少,資料格式頻繁變動。使用seeder能夠快速相應結構變動,但需要更多人力去設計需要被測試的資料。這種方式另一個缺點是效能較差,因為開發人員很可能為了可維護性使用Model建立資料,這樣做的效能是遠遠不及sql和\DB建立資料的。
  2. 使用匯出的資料檔案重建資料庫。在專案或功能成熟後更建議使用這種方式重建資料庫,因為這些資料的“模擬度”更高,填充資料庫的效率更高。
防止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,並在後邊把增減的行顯示出來。

[email protected]:/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/演示

Stub-API 下的介面自動測試

測試覆蓋率不到100的原因有很多,比如測試集較少(沒有覆蓋邊緣引數,導致判斷邊緣引數的程式碼沒有覆蓋),一些判斷production後執行的程式碼沒有覆蓋(if (config('app.env') === 'production'))。

覆蓋率高低並沒有好壞之分,是基於成本的取捨。即使100覆蓋也不能說明測試是完備的,abcdef六種選二邏輯組合,只需要三組就能達到紙面上的100覆蓋,盲目強調覆蓋可能得到自欺欺人的結果。

相對於單元測試的流程

構建基境 -> 設計編寫單元測試程式碼 -> 驗證測試結果

Stub api的測試省略了編寫單元測試程式碼的步驟,轉而直接使用介面進行測試。這樣做測試顯然沒有單元測試的可定製性強,但單元測試有高昂但學習成本和程式碼量,甚至寫單元測試的斷言的工作量就遠遠超過介面測試。Stub api的測試僅需要設定請求引數,如同開發自己除錯程式碼。