Stub-API 下的接口自动测试

前言

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,并在后边把增减的行显示出来。

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/演示

Stub-API 下的接口自动测试

测试覆盖率不到100的原因有很多,比如测试集较少(没有覆盖边缘参数,导致判断边缘参数的代码没有覆盖),一些判断production后执行的代码没有覆盖(if (config('app.env') === 'production'))。

覆盖率高低并没有好坏之分,是基于成本的取舍。即使100覆盖也不能说明测试是完备的,abcdef六种选二逻辑组合,只需要三组就能达到纸面上的100覆盖,盲目强调覆盖可能得到自欺欺人的结果。

与单元测试比较

相对于单元测试的流程

构建基境 -> 设计编写单元测试代码 -> 验证测试结果

Stub api的测试省略了编写单元测试代码的步骤,转而直接使用接口进行测试。这样做测试显然没有单元测试的可定制性强,但单元测试有高昂但学习成本和代码量,甚至写单元测试的断言的工作量就远远超过接口测试。Stub api的测试仅需要设置请求参数,如同开发自己调试代码。

本作品采用《CC 协议》,转载必须注明作者和本文链接
为码农摸鱼事业而奋斗
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!