前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >web前端好帮手 - Jest单元测试工具

web前端好帮手 - Jest单元测试工具

作者头像
QQ音乐技术团队
发布2020-06-15 09:40:27
4.9K0
发布2020-06-15 09:40:27
举报
文章被收录于专栏:QQ音乐技术团队的专栏

本文介绍如何使用Jest覆盖Web前端单元测试、如何统计测试覆盖率,Jest对比Mocha等内容。

Jest是什么?

Jest是一个令人愉快的 JavaScript 测试框架,专注于简洁明快。

正如官方介绍所说,Jest是一款开箱即用的测试框架,其中包含了Expect断言接口、Mock接口、Snapshot快照、测试覆盖率统计等等全套测试功能。

为什么不推荐Mocha?

  • 不支持原生并行测试
  • 断言库要另外安装
  • 测试覆盖率统计功能要另外安装
  • 原生输入的测试报告可读性很差,格式化也要另外安装
  • 不支持snapshot,要另外安装第三方插件

Mocha使用过程中要安装大量第三方模块安装维护,这个过程繁琐并且容易出问题。以至于我每次想写Mocha单元测试时,都要花半天去重读他的文档,这个过程让我逐渐地变得“害怕”写单元测试。

而现在只需要运行npm install -D jest一键安装Jest,便可以快速接入单元测试编写中。

Jest基础使用

项目接入Jest

安装Jest和Jest类型文件,类型文件可以让代码编辑器(如Webstorm)提供Jest相关接口的参数提示:

代码语言:javascript
复制
npm install -D jest @types/jest

在项目目录下创建jest.config.js,配置参考官网。

在packages.json配置命令行接口:

代码语言:javascript
复制
{
  "scripts": {
     "test": "jest",
     "test-debug": "node --inspect-brk node_modules/jest/bin/jest.js --runInBand"
  }}

其中npm run test-debug path/to/xx.test.js接口是用在chrome://inspect上进行断点调试的,后面调试章节会具体介绍。

执行npm run jest命令后就可以跑起项目单元测试了。

一个简单的测试

假设项目中common/url.js文件有两个parse(url:string)``getParameter(url:string)方法需要覆盖单元测试:

代码语言:javascript
复制
const url = require("./common/url.js");

describe("url.parse", () => {
  test("解析一般url", () => {    const uri = url.parse("http://kg.qq.com/a/b.html?c=1&d=%2F");
    expect(uri.protocol).toBe("http:");
    expect(uri.hostname).toBe("kg.qq.com");    // ...
  });
  test("解析带hash的url", () => {...});
  test("解析url片段", () => {...});
});

describe("url.getParameter", () => {
  test("从指定url中获取查询参数", () => {
    expect(url.getParameter("test", "?test=hash-test")).toBe("hash-test");    // ...
  });
  test("从浏览器地址中获取查询参数", () => {...});
  test("当url中参数为空时", () => {...});
  test("必须decodeURIComponent", () => {...});
});

能看到,describe()方法是用来分组(划分作用域)的,第一个参数是分组的名字,每个分组下又包含多个test()来对每个功能点进行详细的测试。基于以上划分,测试逻辑和范围就很清晰了:

  • url.parse方法支持:
    • 解析一般url
    • 解析带hash的url
    • 解析url片段
  • url.getParameter方法支持:
    • 从指定url中获取查询参数
    • 从浏览器地址中获取查询参数
    • 当url中参数为空时
    • 获取url参数返回值经过decode

Webstorm测试界面能看到清晰的分组:

合理的describe()分组和按功能细分test()测试对日后维护起到很关键的作用。

断言库常用接口

Jest内置Expect断言库,下面列举几个常用的断言方法就足以应付正常测试场景。

expect.toBe方法用在全等于判断的场景,类似JS的===全等符号:

代码语言:javascript
复制
expect(1).toBe(1); // 测试通过expect({}).toBe({}); // 报错,因为{} !== {}

expect.toStrictEqual,深度遍历对比两个对象的结构是否全相等:

代码语言:javascript
复制
expect({}).toStrictEqual({}); // 通过expect({
  person: {
    name: "shanelv"
  }
}).toStrictEqual({
  person: {
    name: "shanelv"
  }
}); // 通过

expect.toThrow方法用于测试“错误抛出”:

代码语言:javascript
复制
// 假设urlParse函数对参数校验非法报错function fetchUserInfo(uid) {  if (!uid) {    throw(new Error("require uid!"));
  }  // ...}// 正确写法test('必要参数uid漏传报错', () => {
  expect(() => {
    fetchUserInfo()
  }).toThrow();
});// 错误写法test('必要参数uid漏传报错', () => {
  expect(fetchUserInfo()).toThrow();
});

注意测试错误抛出时,要在测试逻辑外加一层函数包裹,Jest才能捕获到错误。否则像第二种“错误写法”,只会造成JS报错,中断测试运行。

异步处理和超时处理

前端代码异步逻辑太常见了,比如文件操作、请求、定时器等。Jest支持callback和Promise两种场景的异步测试。

首先类似原生NodeJS接口的callback场景,如文件读写:

代码语言:javascript
复制
const fs = require("fs");
test("测试callback读写接口", (done) => {
  fs.writeFileSync("./test.txt", "123456");
  fs.readFile("./test.txt", (err, data) => {
    expect(data.toString()).toBe("123456");
    done();
  });
});

如果是promise异步逻辑,推荐用async/await的写法测试:

代码语言:javascript
复制
const fs = require("fs");const util = require("util");const writeFile = util.promisify(fs.writeFile);const readFile = util.promisify(fs.readFile);

test("测试promise读写接口", async () => {
  await writeFile("./test.txt", "333");  let data = await readFile("./test.txt");
  expect(data.toString()).toBe("333");
});

注意,Jest检测到异步测试时(比如使用了done或者函数返回promise),Jest会等待测试完成,默认等待时间是5秒,如果异步操作时长超过,我们需要通过jest.setTimeout设置等待时长。

我们先来看个超时的例子,将超时时间设置为1秒,但休眠2秒钟,最终休眠还未结束,Jest就中断了测试,并提示超时异常:

代码语言:javascript
复制
function sleep(time) {  return new Promise(resolve => {
    setTimeout(resolve, time);
  });
}// 该测试会报错:Async callback was not invoked within the 1000ms timeout specified by jest.setTimeout.test("超时", async () => {
  jest.setTimeout(1000);
  await sleep(2000);
  expect(1).toBe(1);
});

我们将上面的例子超时设置为3秒,该测试就能顺利通过:

代码语言:javascript
复制
function sleep(time) {  return new Promise(resolve => {
    setTimeout(resolve, time);
  });
}
test("增加Jest的超时时间", async () => {
  jest.setTimeout(3000); // <-- 修改3秒钟
  await sleep(2000);
  expect(1).toBe(1);
});

钩子和作用域

测试时难免有些重复的逻辑,比如我们测试读写文件时需要准备个临时文件,或者比如下面我们使用afterEach钩子,在每个测试完成后重置全局变量:

代码语言:javascript
复制
global.platform = {};function setGlobalPlatform(key, value) {
  global.platform[key] = value;
}

describe("platform", () => {  // afterEach在每个测试完成后触发回调
  afterEach(() => {
    global.platform = {};    console.log("reset platform!");
  });

  test("设置平台信息", () => {
    setGlobalPlatform("ios", true);
    expect(global.platform).toStrictEqual({
      ios: true
    });
  });

  test("设置平台信息为空值", () => {
    setGlobalPlatform("web");
    expect(global.platform).toStrictEqual({
      web: global.undefined
    });
  });
});

通过日志能看到,总共两个测试用例,也触发了两次reset platform逻辑。

Jest还有beforeEachbeforeAll,afterAll等钩子。

Jest钩子只对所在分组下的测试生效,比如:

代码语言:javascript
复制
// 在文件全局作用域下,对该文件中所有测试用例生效afterEach(() => {...});

describe("group-A", () => {  // 在group-A作用域下,对group-A以及group-B的测试用例生效
  beforeEach(() => {})

  describe("group-B", () => {    // 在group-B作用域下,仅对group-B下测试用例生效
    beforeEach(() => {})
  });
});

以上Jest的基础使用介绍,足够应付大部分的场景,下面将针对Jest特性、具体使用心得进行介绍。

合理使用Snapshot

Jest snapshot(快照)原本是用来测试React 虚拟vdom结构的,利用expect(value).toMatchSnapshot([快照名称])将复杂的vdome结构缓存到__snapshots__目录下,之后每次测试都会把运行结果和快照内容进行对比差异,无差异则证明测试通过。

当然其他复杂的结构也可以用快照进行测试,比如文件内容、html、AST、请求内容等:

代码语言:javascript
复制
expect(generateAst("./test.jce")).toMatchSnapshot("test.jce文件的AST结构");

Jest提供快速更新快照功能,npm场景下,我们用下面的命令来更新快照

代码语言:javascript
复制
npm run jest -- --updateSnapshot
# 或者
npm run jest -- -u

这个命令会把本次测试的实际结果更新到快照缓存文件中。

更新快照功能的坏处就是它操作太简单了,简单到让人麻痹,让人懒惰,让人容易忽略快照更新前后的差异对比,将错误的测试结果作为正确快照提交上库

所以这里推荐,第一,尽量让快照简洁可读,方便后续维护时更新快照差异可review。第二,内容少的数据尽量用.toStrictEqual(...)来覆盖,不要用快照。

行内快照怎么用?

和普通快照生成文件不同,行内快照会将快照内容直接打印到测试代码中:

代码语言:javascript
复制
// 运行前:expect({ name: "shanelv" }).toMatchInlineSnapshot();// 运行Jest工具进行测试后,生成的行内快照:expect({ name: "shanelv" }).toMatchInlineSnapshot(`  Object {    "name": "shanelv",
  }
`)

不推荐使用行内快照进行覆盖测试,因为--updateSnapshot也会更新行内快照的内容,上面已经提到过这里的风险。

正确的使用姿势应该是,我们用.toMatchInlineSnapshot()生成行内快照后,再改成.toStrictEqual()方法。

代码语言:javascript
复制
// 运行前:expect(value).toMatchInlineSnapshot();// 运行Jest工具进行测试后,生成的行内快照:expect(value).toMatchInlineSnapshot(`  Object {    "name": "shanelv",
  }
`);// 将行内快照结果改成toStrictEqual方法!!expect(value).toStrictEqual({  "name": "shanelv",
});

这里改成.toStrictEqual()方法的原因有二,第一,用行内快照的场景意味着快照内容短,同样适合.toStrictEqual()方法来维护;第二,将自动更新改为手工更新,增加维护成本,降低错误测试被提交的风险。

另外,要注意系统路径的差异,可能会造成Mac上编写的测试在Windows上却运行失败:

代码语言:javascript
复制
// window的路径,在Mac上会报错expect(value).toMatchInlineSnapshot(`  Object {    "filePath": "f:\\code\\kg\\test.js",
  }
`);// 改成toStrictEqual时,记得把路径信息改过来expect(value).toStrictEqual({  "filePath": path.resolve(__dirname, "./test.js"),
});

什么情况不适用快照?

明确的功能点测试不要用快照,比如下面我们明确要测试setName方法是否能成功设置name属性时,这种情况不应该用快照:

代码语言:javascript
复制
test("setName方法改变name属性“, () => {
  let person = new Person({
    name: "lxj"
    job: "web"
  });
  person.setName("shanelv");

  // 不要用快照
  // expect(person).toMatchSnapshot("用户")

  // 对具体功能进行测试
  expect(person.name).toBe("shanelv")
});

这里我们不需要使用快照记录person实例的其他属性,只需要测试name属性,所以明确的测试点用明确的代码去覆盖,这种场景不要用快照。

其次内容少的数据不要快照,用.toStrictEqual(),上面反复提到过了。

快照命名是个好习惯

.toMatchSnapshot()默认按顺序来命名快照,在实际测试过程中,这样的命名不可读,也让人很难推测出具体是哪句测试代码出问题,造成维护困难。

另外同一个测试下包含多个快照时,由于默认强依赖顺序命名,此时我们改变.toMatchSnapshot()代码的顺序也会造成快照对比报错。

所以推荐大家用.toMatchSnapshot([快照名称])给快照设置命名,在差异对比就能一眼看出是哪句测试代码出问题了,也不会有维护的问题。

React组件如何覆盖测试?

首先安装react-test-renderer库,该库支持将React组件渲染为纯JS对象:

代码语言:javascript
复制
npm install -D react-test-renderer

举个简单的例子:

代码语言:javascript
复制
const renderer = require("react-test-renderer");
test("测试React组件渲染", () => {  let renderInstance = renderer.create(    <div>
      hello Jest    </div>
  );

  let nodeJson = renderInstance.toJSON();

  expect(nodeJson.type).toBe("div");
  expect(nodeJson.children[0]).toBe("hello Jest");
});

注意,如果redux状态组件测试时,要先初始化store和触发redux的事件后,再渲染React组件:

代码语言:javascript
复制
test("init", () => {  let store = initStore(combineReducers(reducer));  /**
   * 先处理store状态,再进行render
   */
  store.dispatch({
    type: "xxx"
  });  let renderInstance = renderer.create(    <Provider store={store}>
      <MyCom />
    </Provider>
  );

  /**
   * React渲染后,再改变store状态不会重新渲染
   */
  //store.dispatch(
  //  type: "xxx"
  //);

  let nodeJson = renderInstance.toJSON();

  // ...
});

这是因为react-test-renderer渲染和服务端渲染类似,渲染只会执行一次,即使渲染过程中触发数据状态变动,也不会再次进行渲染,所以我们一开始要先处理store状态,再渲染React组件。

测试覆盖率统计

Jest自带测试覆盖率功能,在jest.config.js配置文件中开启即可:

代码语言:javascript
复制
// jest.config.jsmodule.export = { // ...
 collectCoverage: true,
};

开启测试覆盖后,我们执行Jest测试完成就会在项目根目录生成一个coverage目录,用浏览器打开其中的index.html文件查看测试覆盖报告。

指定文件统计覆盖率

如果我们需要对项目某几个文件进行测试覆盖率统计,排除其他文件。

比如全民K歌前端这边,我们希望逐步的覆盖业务公共代码的测试,并且要求经过测试的文件覆盖率100%,日后新增代码功能时,已测试文件的覆盖率不能下降(即要求新增功能同时新增对应的测试),我们可以这样设置jest.config.js配置:

代码语言:javascript
复制
/**
 * 以下文件已覆盖测试,改动以下代码要同时加上测试,避免测试覆盖率降低
 */let coverTestFiles = [  "library/client-side/cookie.js",  "library/client-side/url.js",  "library/h5-side/components/lazy.js",  // ...];module.export = { // ...
 collectCoverage: true, // 指定覆盖文件
 collectCoverageFrom: coverTestFiles, // 要求覆盖文件的覆盖率100%
 coverageThreshold: coverTestFiles.reduce((obj, file) => {
   obj[file] = {
     statements: 100,
     branches: 100
   };   return obj;
 }, {}),
};

上面覆盖的文件如果覆盖率低于100%,Jest就会报错,从而中断代码提交或仓库CI合入。

如何“行内“跳过测试覆盖

特殊情况下,我们需要跳过文件中某几句代码的测试覆盖率统计:

代码语言:javascript
复制
/* istanbul ignore else: 跳过else分支的覆盖统计  */if (isNaN(value)) {    // ...} else {   // ...}/* istanbul ignore if: 跳过if分支的覆盖统计  */if (isNaN(value)) {    // ...}

具体看istanbul文档介绍

注意,一般来说,无法覆盖的情况都是因为功能代码编写方式的问题,尽量尝试改进功能代码的编写方式来满足测试需求,避免跳过测试覆盖统计。

Webstorm —— Jest最好的调试工具

Webstorm调试Jest测试非常便利,事实上,上文中测试截图都是在Webstorm上运行的结果,在运行、调试两个方面,Webstorm体验都比node-inspect要更友好。

Webstorm支持测试断言期望结果和实际结果的对比,并弹窗展示完整的结果:

Webstorm支持断点调试Jest,在测试代码左侧打断点,点击debug按钮后,进入调试模式,支持查看变量状态、临时脚本执行等等功能,和chrome调试相差无几,再也不用担心chrome://inspect没有中断断点,端口占用,卡顿、占内存等问题了:

Jest并发实例注意事项

当初Jest推出的亮点之一就是运用并发优势大大加快了测试运行速度。Jest默认情况下是开启并发的,我们不需要另外配置启用就能享受测试的高速便利。这里要介绍的是Jest并发时的两点注意事项。

首先,由于Jest启动多个进程,并发地跑测试,我们使用node-inspect的方式去跑断点调试时,chrome://inspect页面上断点不会被中断,导致我们无法断点调试。此时我们要在Jest命令后面加个--runInBand参数(在上述“项目接入”章节下也有展示):

代码语言:javascript
复制
{
  "scripts": {
     "test-debug": "node --inspect-brk node_modules/jest/bin/jest.js --runInBand"
  }}

--runInBand参数让Jest在同一个进程下运行测试,方便我们断点调试。

当然如果用Webstorm调试Jest就无需担心这种并发的情况,WebStorm默认走单进程执行Jest。

第二点,由于Jest测试都是并发运行的,有些外部资源处理要注意隔离,比如文件处理、数据库、本地缓存、请求之类的。下面例子中就是两个测试用例对一个文件进行测试:

代码语言:javascript
复制
function bigContent() {  let arr = [];  for (let i = 0; i < 1000000; i++) {
    arr.push(String(Math.random()));
  }  return arr.join(" ");
}// test-a.test.jstest("测试文件内容-A", async () => {  let content = bigContent();  // let fileName = "./test.txt"; // 危险!!
  let fileName = "./test-a.txt"; // 名称隔离
  await writeFile(fileName, content);  let data = await readFile(fileName);
  expect(data.toString()).toBe(content);
});// test-a.test.jstest("测试文件内容-B", async () => {  let content = bigContent();  // let fileName = "./test.txt"; // 危险!!
  let fileName = "./test-b.txt"; // 名称隔离
  await writeFile(fileName, content);  let data = await readFile(fileName);
  expect(data.toString()).toBe(content);
});

其他方面

Jest Mock很关键也很常用,大家可以参考下官方文档,了解下面的场景并实际运用到项目:

  • mock函数
    • 捕获运行情况
    • 定义函数实现
  • mock模块
    • 自动mock模块
    • 自定义模块

单元测试之于开发

开发掌握单元测试,犹鱼之有水。我们大可把重复的测试操作交给自动化测试逻辑来负责,减少手动操作的时间,有种说法也是这般道理:先写测试,后写代码。说白了就是,先规划好实际使用的场景,再用代码去实现他。

而相反的想一步写一步代码,可能容易出现api参数反复修改、功能和实际情况不匹配、边界情况考虑不周等来回返工的情况。

甚至可以说,在单元测试覆盖良好/完全的项目中,我们可以把”Code Review“的侧重点转移到单元测试覆盖上,即只要保证单元测试覆盖良好,功能代码多个空格少个空格、你爱用switch-case我爱用if-else、代码可读性差到媲美压缩级别代码等等都已无关紧要。

单元测试之于开发就是这般的重要。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-06-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 腾讯音乐技术团队 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Jest是什么?
    • 为什么不推荐Mocha?
    • Jest基础使用
      • 项目接入Jest
        • 一个简单的测试
          • 断言库常用接口
            • 异步处理和超时处理
              • 钩子和作用域
              • 合理使用Snapshot
                • 行内快照怎么用?
                  • 什么情况不适用快照?
                    • 快照命名是个好习惯
                    • React组件如何覆盖测试?
                    • 测试覆盖率统计
                      • 指定文件统计覆盖率
                        • 如何“行内“跳过测试覆盖
                        • Webstorm —— Jest最好的调试工具
                        • Jest并发实例注意事项
                        • 其他方面
                        • 单元测试之于开发
                        领券
                        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档