Node 自动化单元测试

2,369 阅读5分钟

想法缘由

为什么会想做自动化单元测试呢?主要的原因还是--

先交代大背景,便于后续展开:

  • 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层项目")的话,可得到内外部依赖的解析图:

  1. 函数代码有 TS 类型定义(完善程度不一)。通过 TS 的类型定义,使用工具可推导出对应的 mock 数据,即:可 mock 出对应的函数入参数据
  2. 用户信息可以写死,通常情况下不需要做改变,可 mock
  3. RPC 信息可借助 GULU 官方插件 @byted-service/rpc-mock 进行 mock

这样思考下来,可以得到一个结论:service层项目的所有外部依赖(输入)都是可以进行 mock的。

将 service 层项目看作黑盒,在输入不变的情况下,输出应该不变!

具体方案

将上面的萌芽想法经过充分讨论后,得到一个整体逻辑图:

方案详细说明

获取函数入参类型与 Mock 数据

借助 TS 编译器能力可获取函数输入输出参数格式。

借助工具 typescript-json-schemajson-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$);
  });
});

落地&挑战

最终输出

  1. 开发并发布 kobs(插件名) 库到公司 NPM。
  2. 输出一份自动化单测 kobs 使用指南。

落地情况

  1. 建立 node_core 单测接入 CI 新流程
  2. 先后在数据报表、工具两个方向进行了接入:代码行覆盖率0% 88%

  1. 截获因单测不通过的代码误修改的上报案例 2

Demo 演示

挑战

  1. 自动化测试这个过程,如何确保把正常业务流程做了测试保障呢?

在自动化测试阶段,无法保证将正常业务流程做了保障。分两个点:

  • 正常业务流程的职责大部分交由 QA/集成测试来保证。
  • 自动化测试可将某些不易察觉的边界情况做到测试:增强代码鲁棒性、保障代码重构质量。
  1. 如何将该工具与人工手写测试的步骤结合起来呢?

有过对应尝试,不过最终的结合方式都不太符合预期。现阶段,提供了一个 vscode 插件工具给到开发者,辅助其生成自动化单测的模板(包含mock 的 入参数据、rpc 初始数据)。

回顾思考

  1. 项目建设的经验总结: 代码书写规范性要求、模块结构划分明晰的重要性
  2. 是否真的可以不用写单元测试了呢?❌
  3. 最终方案定位:辅助生成测试工具。