想法缘由
为什么会想做自动化单元测试呢?主要的原因还是--懒
先交代大背景,便于后续展开:
-
2020年10月,平台侧开始将后端接口迁移到 BFF 层(以下简称"项目")
-
项目代码包含多个方向:创编、BP、数据报表&工具、推广管理、素材库
-
项目代码使用 TS 编写,TS定义完善程度在不同方向规范程度不一(未做全局 ts 定义规范限制)
-
项目与后端接口交互:95%+ 是 RPC 的方式,极少部分使用 http 方式
-
项目使用 GULU 框架(公司内部框架,类似 Egg),文件结构包括:router、controller、service等
-
请求调用顺序为:前端请求 -> router -> controller -> service
-
不存在逆向调用的情况
-
controller层与service层的分工做了初步明确:
- controller 层负责:权限校验、请求入参处理、结果格式化返回
- service 层负责:后端 RPC 交互、数据裁剪、拼接
-
-
项目未使用 redis / DB 存储
-
项目未包含单元测试
手写阶段
没有单元测试做代码的质量保障,不安心。在单元测试的手写落地阶段,明确针对 service 层写单元测试。
经过一番手写的尝试,一些单元测试在落地上的老问题暴露出来了:
不熟悉:对jest的用法不熟悉、对GULU 测试写法不熟悉、如何写测试也不熟悉
效果衡量:单测不属于业务的一部分,动力不足。单测效果衡量也缺乏指标
人力投入:对于新手来说,写一个覆盖率达标(预期80%)的单测,需要花费约 1/3 的开发时间
基于这些问题,写单测的积极性不太高。需要从上往下强推。开始思考:是否可以让单元测试自动化呢?
萌芽
如果从 service 这一层作为入口,来看整个项目(以下简称"service层项目")的话,可得到内外部依赖的解析图:
- 函数代码有 TS 类型定义(完善程度不一)。通过 TS 的类型定义,使用工具可推导出对应的 mock 数据,即:可 mock 出对应的函数入参数据
- 用户信息可以写死,通常情况下不需要做改变,可 mock
- RPC 信息可借助 GULU 官方插件
@byted-service/rpc-mock进行 mock
这样思考下来,可以得到一个结论:service层项目的所有外部依赖(输入)都是可以进行 mock的。
将 service 层项目看作黑盒,在输入不变的情况下,输出应该不变!
具体方案
将上面的萌芽想法经过充分讨论后,得到一个整体逻辑图:
方案详细说明
获取函数入参类型与 Mock 数据
借助 TS 编译器能力可获取函数输入输出参数格式。
借助工具 typescript-json-schema 与 json-schema-faker 可得到 Mock 数据。其中,typescript-json-schema 支持最新 TS 编写格式,满足日常 TS 格式要求。
环境构建
前提:被测项目(基于 Gulu)的测试框架已经搭建完成。
借助已经配置好的测试框架进行环境的构建,不直接侵入已有代码逻辑:
import { getFrameworkPath, restore } from './lib/util';
import { HttpApplication, HttpApplicationOptions } from '@gulu/application-http';
const frameworkName = '@gulu/application-http';
const initCwd = process.cwd();
// 启动被测 Gulu 项目的进程
export const app = function (options: HttpApplicationOptions = {}): HttpApplication {
const root = options.root || initCwd;
// 启动 root 路径(或当前路径)下的 Gulu 应用
const { HttpApplication: Application } = require(getFrameworkPath(frameworkName, root));
const application = new Application({
...options,
// 设置为 test 测试配置
guluEnv: {name:'test',value:['test']},
root,
});
application.load(root);
application.listen(0);
return application;
};
// 调用 app
const mocker = app();
await mocker.ready();
const ctx = mocker.createMockContext();
插桩设置
现阶段前端最通用的插桩工具为 istanbul/nyc。它提供了两种插桩方式:
- 编译时插桩: 即在代码转译过程中插入覆盖率采集代码,产出代码本身即拥有采集能力。
- 运行时插桩:即产出代码本身不具有采集能力,使用
hookRequire方法(该方法借助 Nodejs 的Module加载机制,hook被 require 引入的文件,返回插桩后的文件)。因此需要在业务代码 require 前引入。
本方案采用运行时插桩方式来进行:
import NYC from 'nyc';
const n = new NYC(config);
n.reset();
n.wrap(); // hookRequire,运行时插桩
// 运行核心逻辑程序
if (coverageImproved) {
n.writeCoverageFile();
}
测试用例生成
断言结果
测试用例分为三个阶段:排列资源(Arrange) 、执行行为(Act) 、断言结果(Assert)。 在自动化单元测试中,断言结果采取的是 toMatchObject() 用于判断实际执行的输出与单测的输出是否发生数据变化(子集关系)。
用例生成方式
模板替换。以标记符替换的方式来进行变量替换:
describe( '$__className$自动生成单测', () => {
it('$__functionName$函数正常返回', () => {
let ctx = app.mockContext();
ctx.request = Object.assign(ctx.request, loginInfo);
let resp = ctx.service.$__functionName$.apply(null,$__arguments$);
expect(resp).toMatchObject($__respData$);
});
});
落地&挑战
最终输出
- 开发并发布 kobs(插件名) 库到公司 NPM。
- 输出一份自动化单测 kobs 使用指南。
落地情况
- 建立 node_core 单测接入 CI 新流程
- 先后在数据报表、工具两个方向进行了接入:代码行覆盖率为
0%88%
- 截获因单测不通过的代码误修改的上报案例 2 起
Demo 演示

挑战
-
自动化测试这个过程,如何确保把正常业务流程做了测试保障呢?
在自动化测试阶段,无法保证将正常业务流程做了保障。分两个点:
- 正常业务流程的职责大部分交由 QA/集成测试来保证。
- 自动化测试可将某些不易察觉的边界情况做到测试:增强代码鲁棒性、保障代码重构质量。
-
如何将该工具与人工手写测试的步骤结合起来呢?
有过对应尝试,不过最终的结合方式都不太符合预期。现阶段,提供了一个 vscode 插件工具给到开发者,辅助其生成自动化单测的模板(包含mock 的 入参数据、rpc 初始数据)。
回顾思考
- 项目建设的经验总结: 代码书写规范性要求、模块结构划分明晰的重要性
- 是否真的可以不用写单元测试了呢?❌
- 最终方案定位:辅助生成测试工具。