深入浅出 Jest 框架的实现原理
https://github.com/Wscats/jest-tutorial
什么是 Jest
Jest 是 Facebook 开发的 Javascript 测试框架,用于创建、运行和编写测试的 JavaScript 库。
Jest 作为 NPM 包发布,可以安装并运行在任何 JavaScript 项目中。Jest 是目前前端最流行的测试库之一。
测试意味着什么
在技 术术语中,测试意味着检查我们的代码是否满足某些期望。例如:一个名为求和(sum
)函数应该返回给定一些运算结果的预期输出。
有许多类型的测试,很快你就会被术语淹没,但长话短说的测试分为三大类:
- 单元测试
- 集成测试
- E2E 测试
我怎么知道要测试什么
在测试方面,即使是最简单的代码块也可能使初学者也可能会迷惑。最常见的问题是“我怎么知道要测试什么?”。
如果您正在编写网页,一个好的出发点是测试应用程序的每个页面和每个用户交互。但是网页其实也需要测试的函数和模块等代码单元组成。
大多数时候有两种情况:
- 你继承遗留代码,其自带没有测试
- 你必须凭空实现一个新功能
那该怎么办?对于这两种情况,你可以通过将测试视为:检查该函数是否产生预期结果。最典型的测试流程如下所示:
- 导入要测试的函数
- 给函数一个输入
- 定义期望的输出
- 检查函数是否产生预期的输出
一般,就这么简单。掌握以下核心思路,编写测试将不再可怕:
输入 -> 预期输出 -> 断言结果。
测试块,断言和匹配器
我们将创建一个简单的 Javascript 函数代码,用于 2 个数字的加法,并为其编写相应的基于 Jest 的测试
const sum = (a, b) => a + b;
现在,为了测试在同一个文件夹中创建一个测试文件,命名为 test.spec.js
,这特殊的后缀是 Jest 的约定,用于查找所有的测试文件。我们还将导入被测函数,以便执行测试中的代码。Jest 测试遵循 BDD 风格的测试,每个测试都应该有一个主要的 test
测试块,并且可以有多个测试块,现在可以为 sum
方法编写测试块,这里我们编写一个测试来添加 2 个数字并验证预期结果。我们将提供数字为 1 和 2,并期望输出 3。
test
它需要两个参数:一个用于描述测试块的字符串,以及一个用于包装实际测试的回调函数。expect
包装目标函数,并结合匹配器 toBe
用于检查函数计算结果是否符合预期。
这是完整的测试:
test("sum test", () => {
expect(sum(1, 2)).toBe(3);
});
我们观察上面代码有发现有两点:
test
块是单独的测试块,它拥有描述和划分范围的作用,即它代表我们要为该计算函数sum
所编写测试的通用容器。expect
是一个断言,该语句使用输入 1 和 2 调用被测函数中的sum
方法,并期望输出 3。toBe
是一个匹配器,用于检查期望值,如果不符合预期结果则应该抛出异常。
如何实现测试块
测试块其实并不复杂,最简单的实现不过如下,我们需要把测试包装实际测试的回调函数存起来,所以封装一个 dispatch
方法接收命令类型和回调函数:
const test = (name, fn) => {
dispatch({
type: "ADD_TEST", fn, name });
};
我们需要在全局创建一个 state
保存测试的回调函数,测试的回调函数使用一个数组存起来。
global["STATE_SYMBOL"] = {
testBlock: [],
};
dispatch
方法此时只需要甄别对应的命令,并把测试的回调函数存进全局的 state
即可。
const dispatch = (event) => {
const {
fn, type, name } = event;
switch (type) {
case "ADD_TEST":
const {
testBlock } = global["STATE_SYMBOL"];
testBlock.push({
fn, name });
break;
}
};
如何实现断言和匹配器
断言库也实现也很简单,只需要封装一个函数暴露匹配器方法满足以下公式即可:
expect(A).toBe(B)
这里我们实现 toBe
这个常用的方法,当结果和预期不相等,抛出错误即可:
const expect = (actual) => ({
toBe(expected) {
if (actual !== expected) {
throw new Error(`${
actual} is not equal to ${
expected}`);
}
}
};
实际在测试块中会使用 try/catch
捕获错误,并打印堆栈信息方面定位问题。
在简单情况下,我们也可以使用 Node 自带的 assert
模块进行断言,当然还有很多更复杂的断言方法,本质上原理都差不多。
CLI 和配置
编写完测试之后,我们则需要在命令行中输入命令运行单测,正常情况下,命令类似如下:
node jest xxx.spec.js
这里本质是解析命令行的参数。
const testPath = process.argv.slice(2)[0];
const code = fs.readFileSync(path.join(process.cwd(), testPath)).toString();
复杂的情况可能还需要读取本地的 Jest 配置文件的参数来更改执行环境等,Jest 在这里使用了第三方库 yargs
execa
和 chalk
等来解析执行并打印命令。
模拟
在复杂的测试场景,我们一定绕不开一个 Jest 术语:模拟(mock
)
在 Jest 文档中,我们可以找到 Jest 对模拟有以下描述:”模拟函数通过抹去函数的实际实现、捕获对函数的调用,以及在这些调用中传递的参数,使测试代码之间的链接变得容易“
简而言之,可以通过将以下代码片段分配给函数或依赖项来创建模拟:
jest.mock("fs", {
readFile: jest.fn(() => "wscats"),
});
这是一个简单模拟的示例,模拟了 fs 模块 readFile 函数在测试特定业务逻辑的返回值。
怎么模拟一个函数
接下来我们就要研究一下如何实现,首先是 jest.mock
,它第一个参数接受的是模块名或者模块路径,第二个参数是该模块对外暴露方法的具体实现
const jest = {
mock(mockPath, mockExports = {
}) {
const path = require.resolve(mockPath, {
paths: ["."] });
require.cache[path] = {
id: path,
filename: path