写点什么

搭建基本 Jest 测试框架,解读覆盖率实现原理

作者:梁龙先森
  • 2021 年 12 月 05 日
  • 本文字数:3809 字

    阅读完需:约 12 分钟

搭建基本Jest测试框架,解读覆盖率实现原理

前端自动化测试,在写通用库的时候,为了减少后续更新迭代需要的测试投入,以及保证库功能的健壮性,通常都要带上的。当然在实现部分业务代码,可能也会考虑写测试用例,只不过这块的投入成本往往比较大,投入产出性价比不够高,因此一般只会使用在核心功能上。


前端自动化测试一般分为 2 种:单元测试和 e2e 测试(UI 自动化测试)。e2e 测试主流框架有:selenium、cypress、puppeteer 等;单元测试主流测试框架有:JasmineMochaJest等。它们都很优秀,易用性也很强,功能也强大。但本文这里只聊聊 Jest 这个框架。

一、Jest 优点

Jest 是 Facebook 的一套开源 JavaScript 测试框架,专注于简洁明快。像BabelTypeScriptNodeReact 等优秀的项目都在使用它。它有几大优秀:

  1. 零配置在大部分 JavaScript 项目上实现开箱即用,无需配置。

  2. 快照

  3. 能够轻松追踪大型对象的测试。快照可以与测试代码放一起,也可以集成进代码行内。

  4. 隔离

  5. 测试程序拥有自己独立的进程,以最大限度地提高性能。

  6. 优秀的 Api

  7. 从 it 到 expect - Jest 将整个工具包放在同一个 地方。好书写、好维护、非常方便。

  8. 支持覆盖率

  9. 通过添加 --coverage 标志生成代码覆盖率报告, 无需额外设置。


它的优势非常多,不仅于此,感兴趣的可以前往官网查看。

二、Jest 使用和配置

这里的环境我们按照 TypeScript 进行配置。

  1. 安装包

// 1. 安装jestnpm install --save-dev jest// 当然你也可以全局安装npm install jest -g
// 2. 生成基础配置文件jest --init
// 3. 使用babel,为了能够兼容当前Node版本npm install babel-jest @babel/core @babel/preset-env --save-dev
// 4. 使用typescript,所以需要安装对应包npm install @babel/preset-typescript --save-dev
复制代码


  1. babel 的配置如下,需要注意的是:presets 配置,它的执行顺序是从后往前执行的,如果 babel 有配置 plugins 它的执行顺序是从上往下。

// babel.config.jsmodule.exports = {   presets: [     ['@babel/preset-env', {targets: {node: 'current'}}],     '@babel/preset-typescript',   ]};
复制代码


  1. 配置 npm script

// package.json{   "scripts": {      "test": "jest"   }}
复制代码

配置到这,基本的 Jest 测试框架环境就好,执行 npm run test 就会执行测试用例了。

三、编写测试用例

如何编写测试用例,已经存在很多教程,官方也有非常多例子,因此这里我们只写两种很常用的:同步代码和异步代码的测试用例。

  1. 同步代码

// sum.test.tsconst sum = (a:number, b:number) => a + b
test('两数相加', () => { // 推断 1 + 1 = 2 expect(sum(1, 1)).toBe(2)})
复制代码


  1. 异步代码

// async.test.tsconst getData = (type: string) => type === 'get'?Promise.resolve(1):Promise.reject(2)
test('测试异步', async () => { expect.assertions(1); await expect(getData('get')).resolves.toEqual(1)})
复制代码

关于如何编写更多场景的测试用例,可前往官网查看。下面看看如何解读覆盖率。

四、解读覆盖率

通常写完库的测试用例,需要跑下覆盖率,看看测试用例覆盖率如何,jest刚好也支持查看覆盖率,对应的指令是:jest --coverage。当执行该指令,会在当前项目跟目录生成覆盖率文件夹coverage如下:

找到lcov-report/index.html文件,然后在浏览器中打开,此时我们便可以查看当前项目的测试用例书写覆盖率了。这里以我写的工具库为例子:


表格中罗列了所有工具类库的测试用例,存在几下几个指标:

  1. Statements 语句覆盖率,它其实对应的就是 js 语法上的语句,js 解析成 ast 数中类型为statement

  2. Branches 分支覆盖率,通俗点理解就是if/else这类条件

  3. Functions 函数覆盖率

  4. Lines 行数覆盖率,就是代码执行了多少行


文件中,我们发现 url-utils 文件中的 Branches 覆盖率不高,才达到64.29%,那么如何查看具体哪些代码没覆盖到呢?很简单,点击对应的文件名就可以进入查看了,效果如下:

图中黄色块代表的是测试用例没有测试到的方式,当然你也可以按快捷键 n 或者 j 去查看下一个没有被覆盖到的代码块。图中黄块未覆盖到的,刚好也正是条件语句,符合 Branchs 的测试覆盖率。到这里便完成了基本的测试用例编写,以及测试用例生成,并且能够查看测试用例覆盖率如何,以此来完善整体测试用例的编写,帮助代码达到完善健硕的目的。


关于覆盖率是如何生成的,它的原理是怎么样的呢?

五、覆盖率原理

TIP:再进行真正解读原理前,先进行大胆的猜测,然后再去验证它,会比生硬的看原理来得有意义。


通过生成的指标看出 jest 框架生成的覆盖率对语句、函数、分支、行数这 4 个维度进行了生成,如果对 js 编译原理有所了解,根据敏锐度大体能过猜测到应该是需要对 jsast 树解析,因为这几个指标的类型,在对应的 ast 数的节点类型上都有对应的体现。当解析成 ast 树后,应该存在某种机制能往树里面侵入部分统计代码,那么在执行的时候遍可以统计 ast 树哪些类型的节点是已经被执行了。


了解库的实现原理,通常第一步是打开类库的package.json文件,看看它都依赖了什么。通过对该文件的排查,我们排除到这三个库:



  1. istanbul-lib-coverage:提供覆盖率信息的只读视图,能够合并和汇总覆盖率信息的 api。

  2. istanbul-lib-report:istanbul 库生成报告的核心程序。

  3. istanbul-reports:大体提供报告输出的公共能力 api。


这三个库使用围绕着 istanbul ,因此该库基本大概率提供了核心的覆盖率能力。

1、istanbul

istanbul,为 ES5 和 ES2015+JavaScript 代码插入行计数器,这样您就可以跟踪单元测试对代码库的运行情况。


安装使用

npm install -g istanbul
复制代码


安装完成后,执行 istanbul help 能够获取它所支持的命令:

  1. check-coverage:根据 JSON 文件的覆盖率阈值检查总体/每个文件的覆盖率。如果不满足阈值,则退出 1,否则退出 0。

  2. cover:透明地将覆盖率信息添加到节点命令。在执行结束时保存 coverage.json 和报告。

  3. instrument:插入文件或目录树,并将插入指令的代码写入所需的输出位置。

  4. report:为上一次运行中生成的 JSON 对象写入覆盖率报告。

命令中 instrument 名为 “插桩”,跟猜想功能类似。

2、instrument 命令

该指令会往执行代码插入统计辅助代码,该行为称为:函数插桩

下面验证下:

// test.js文件const toSum = (a, b) => {  if (a > 10) {    return a + b + 10;  }  return a;};toSum();
复制代码

test.js 文件执行该命令,并输出到test-inst.js文件:

istanbul instrument ./test.js -o ./test-inst.js
复制代码

转换后的代码,通过格式化后如下:

可以看出如下信息:

  1. 它创建了一个全局对象用于存储统计信息

// 形式如下:var coverages = {  '/root/test.js':{     path: '/root/test.js',     // statement数量     s: {},     // branch数量     b: {},     // function数量     f: {},     // 记录function的开始结束位置     fnMap: {},     // 记录statement的开始结束位置     statementMap: {},     // 记录statement的开始结束位置     branchMap: {}  }}
复制代码
  1. 它往函数执行代码各个对应位置插入了统计代码,对应代码块执行就+1

  2. 命名很复杂,应该是为了防止与命名冲突。

这是生成覆盖率的基本原理。那至于 istanbul 库是如何实现函数插桩功能的呢?

3、函数插桩原理

通过 istanbul 库的 package.json 文件,找到两个重要的库:

  1. escodegen:它是来自 Mozilla 解析器 API AST 的 ECMAScript(也称为 JavaScript)代码生成器

  2. esprima:是一种高性能、标准兼容的 ECMAScript 解析器

同时在源码中可以找到插桩器实现 Instrumenter,它的代码在:lib/instrumenter.js 文件。

下面看下主体流程:

  1. 当执行 instrument 命令,实际上会实例化一个 Instrumenter,在内部会挂在个 this.walker 它的作用是,声明需要执行函数插桩的 AST 节点类型,以及对应的插桩函数,比如: this.coverStatement

  1. 执行它的 instrumentSync函数,通过 esprima 将代码解析成 AST 树,后进行函数插桩操作

  1. 声明 coverState 对象存储覆盖率数据,并对 AST 树进行统计代码插桩操作,并将统计coverState 转换为 JSON 字符串输出。

 instrumentASTSync: function (program, filename, originalCode) {     // 省略了非核心主流程代码     // 1. 初始化对象,用于存放覆盖率统计数据   this.coverState = {     path: filename,     s: {},     b: {},     f: {},     fnMap: {},     statementMap: {},     branchMap: {}   };   // 2. 对AST相应的节点类型进行处理插桩操作   this.walker.startWalk(program);    // 3. 通过escodegen将插桩后的ast树转换为代码字符串    generated = ESPGEN.generate(program, codegenOptions);    // 4. 获取coverState转化为字符串输出    preamble = this.getPreamble(originalCode || '', usingStrict);    return preamble + '\n' + generated + '\n';},
复制代码
  1. 具体的插桩操作

在执行 this.walker.startWalk 时,执行节点的插桩函数 this.coverStatement 嵌入统计代码。

  1. 将覆盖率数据转为字符串输出,这就是前面执行 instrument 命令代码进行插桩后,头部声明的数据。

以上是整个覆盖率原理解读和主体核心流程代码,加而言之就是:将代码转为 AST 树,然后对需要统计覆盖率的 statement/branch/function 等对应的节点类型进行函数插桩操作用于统计对应代码的执行率。

六、总结

通过对 Jest 测试框架的基本搭建,以及解读覆盖率报告和它的实现原理,基本能掌握该框架核心的机制,对于更细致的业务代码测试用例,使用的时候再去找对应的 API 进行学习使用也是可以。

发布于: 2021 年 12 月 05 日阅读数: 19
用户头像

梁龙先森

关注

无情的写作机器 2018.03.17 加入

vite原理/微前端/性能监控方案...,正在来的路上...

评论

发布
暂无评论
搭建基本Jest测试框架,解读覆盖率实现原理