一、引言
前端这几年发展的非常迅速,我们的系统的功能正在变的越来越复杂,这对我们的前端工程化能力提出了更高的要求,听到工程化,大家的第一反应肯定是高质量的代码设计和高质量的代码实现。
但实际上,前端自动化测试也是前端工程化里面非常重要的一个环节。
二、Jest 基础入门
一个普通前端听到自动化测试,第一反应可能是:我工作这么多年也没写过测试,这个东西有用吗?
答:非常有用
如果你打开GitHub
,去看一下流行的开源库或者框架的源码,你会发现,在这些源码里面,全部都包含了大量的自动化测试的代码。比如antd
、lodash
、再比如vue
、react
、echarts
、redux
等等……
开源的工具需要稳定性,而引入前端自动化测试为开源项目提供稳定性,是再好不过的选择了。
三、学习前提
阅读这篇文章需要以下知识储备:
- js、es6 基础语法
- node、npm 相关知识
- git 的相关操作
- react或者vue,至少了解一个
- 状态管理工具,至少了解一个
四、背景及原理
首先在任意目录下创建一个math.js
文件,假设这个文件是一个数学库,里面定义两个函数,分别是加法和减法:
// math.js
function add(a, b) {
return a + b;
}
function minus(a, b) {
return a - b;
}
复制代码
这时候我们可以在业务代码里去使用这个数学库。
但是,假如,上面的minus函数
我们不小心写错了,把减法
写成了乘法
,如果直接在业务代码中使用这个方法,就会带来无法预期的bug。
所以这时候,我们就需要对math.js
这个公共库进行自动化测试
,确保没问题之后,再让业务组件去调用,这样就会保证不会出特别多的bug了。
我们可以这样做:
在该目录下创建一个math.test.js
文件,然后写一点测试代码:
const result = add(3, 7);
const expect = 10;
if (result !== expect) {
throw new Error(`3 + 7 应该等于${expect},结果却是${result}`);
}
复制代码
const result = minus(3, 3);
const expect = 0;
if (result !== expect) {
throw new Error(`3 - 3 应该等于${expect},结果却是${result}`);
}
复制代码
这时候我们运行这段代码,会发现没有抛出任何异常,说明这两个测试用例都通过了。
这就是自动化测试最原始的雏形。
然后我们思考一个问题,如何将这堆代码进行简化,做成一个公用的函数,比如这样:
// 测试 3 + 3 是否等于 6
expect(add(3, 3)).toBe(6);
// 测试 3 - 3 是否等于 0
expect(minus(3, 3)).toBe(0);
复制代码
expect 方法
实现:
function expect(result) {
return {
toBe(actual) {
if (result !== actual) {
throw new Error("预期值和实际值不相等");
}
},
};
}
复制代码
这时候我们运行这段代码,会发现没有抛出任何异常,说明这两个测试用例都通过了。
虽然实现了 expect 函数
,但是报错的内容始终是一样的,我们不知道是具体哪个方法出现了问题,这时候我们就会想到,我们需要将这个 expect 方法进一步做改良,我们如果能在 expect 方法
外部再包装一层,就可以多传递一些额外的内容,比如创造这样的写法:
test("测试加法 3 + 3", () => {
expect(add(3, 3)).toBe(6);
});
test("测试减法 3 - 3", () => {
expect(minus(3, 3)).toBe(0);
});
复制代码
这样封装之后,我们既能进行测试,又能得到测试的描述。
test 方法
实现:
function test(desc, fn) {
try {
fn();
console.log(`${desc} 通过测试`);
} catch {
console.log(`${desc} 没有通过测试`);
}
}
复制代码
所以前端自动化测试到底是什么?
答:实际上就是写了一段其它的用来测试的js代码,通过测试代码去运行业务代码,判断实际结果是否满足预期结果,如果满足,就是没有问题,如果不满足,就是有问题。
上面实现的 expect 方法
和 test 方法
实际上和主流的前端自动化测试框架 jest
里面的语法是完全一致的
。所以上面的示例代码可以理解为 jest
的底层实现原理。
五、Jest 框架入门
在上面的部分,我们实现了 expect 方法 和 test 方法。
在实际项目的自动化测试过程中,如果只有这两个方法,很显然,是远远不够的,这时候,就需要我们对之前的方法进行扩充,同时还有很多自动化的机制需要集成进去。
这时候!Jest
闪亮登场!
5.1 Jest 框架介绍
一个优秀的自动化测试框架,在以下三个方面应该比较突出:
- 性能好
- 功能齐全
- 易用性很好
Jest
在这三个方面做的非常好(当然了,像Jasmine
、mocha
这些主流前端测试框架做的也不错)。
这些主流前端测试框架使用的方法、原理上都差不多,实际上,我们学会了一个,另外几个也就很容易的继续学习了,所以这篇文章只讲解 Jest。
那么 Jest 具体好用在哪些方面呢?
答案是这些:
- 速度快(可以自动监测修改过的代码,不会重复测试)
- API 简单、数量少
- 易配置
- 隔离性好
- 监控模式
- IDE 整合(比如vs code)
- Snapshot(快照测试)
- 多项目并行
- 覆盖率
- Mock 丰富
- 对新技术支持度好
5.2 使用 Jest 修改自动化测试样例
在上面的部分,我们自己写了一些测试代码对 Math
库进行了测试,现在我们要使用 Jest
对 Math 进行重新测试。
首先打开编辑器,进入到控制台(为了避免不必要的麻烦,建议大家跟我一样使用vs code
):
第1步:初始化npm环境:
npm init
在执行命令的时候会遇到一些选项或者问题,无脑按回车即可,如果看到目录下出现了一个 package.json
文件,那么这就是一个标准的 npm
包了。
第2步:安装依赖
npm install jest -D
复制代码
(注:-D 是 --save -dev 的缩写)
如果看到 Jest
出现在了 package.json
文件中,并且目录中出现了 node_modules
文件夹,就说明安装成功了。
第3步:修改之前的代码
首先删除之前写的 test 方法
和 expect 方法
的实现过程,因为 Jest
自带了这两个方法,我们不需要手动实现。然后采用模块化的标准,将 math.js 中的方法导出:
// math.js
function add(a, b) {
return a + b;
}
function minus(a, b) {
return a - b;
}
function multi(a, b) {
return a * b;
}
module.exports = { add, minus, multi };
复制代码
然后在 math.test.js 文件中引入这几个方法:
// math.test.js
const { add, minus, multi } = require("./math");
test("测试加法 3 + 3", () => {
expect(add(3, 3)).toBe(6);
});
test("测试减法 3 - 3", () => {
expect(minus(3, 3)).toBe(0);
});
test("测试乘法 3 * 3", () => {
expect(multi(3, 3)).toBe(9);
});
复制代码
第3步:配置 package.json 文件
将如下代码添加到 package.json
中:
"scripts": {
"test": "jest"
},
复制代码
第4步:执行测试代码
完成配置之后,运行 yarn test
或者 npm run test
,控制台将输出如下信息:
假设这个时候,我们把第三个测试用例的结果改成错误值:
// math.test.js
const { add, minus, multi } = require("./math");
test("测试加法 3 + 3", () => {
expect(add(3, 3)).toBe(6);
});
test("测试减法 3 - 3", () => {
expect(minus(3, 3)).toBe(0);
});
test("测试乘法 3 * 3", () => {
expect(multi(3, 3)).toBe(10); // 错误的结果
});
复制代码
重新运行 yarn test
或者 npm run test
,可以看到如下结果:
可以看到,控制台报出了错误:
乘法测试没有通过,期待值(Expected)是10,但是返回值是(Received)9。
然后我们再把测试结果改成正确的值,重新执行命令行,就能得到正确的测试结果了。
这个时候,说明Jest
这个框架已经被我们正确的使用了。
5.3 Jest 的简单配置
Jest
和 Webpack
一样都有默认配置
,我们可以运行 npx jest --init
命令来
初始化默认配置。在运行命令行的时候,会弹出一些选项,比如:
- 是否需要开启
typescript
- 是否需要生成
覆盖率报告
- 选择
node 环境
或者浏览器环境
- 在测试完成之后,是否需要进行一些
清除工作
- ……
这些选项可以根据自己的实际情况进行选择是
或者否
。
当全部选择完成之后,可以看到在目录中多了一个 jest.config.js
文件,这时候就说明初始化配置完成了。点击这里可以查看配置文档。
1. 代码测试覆盖率:
当我们在控制台中输入npx jest --coverage
时,控制台会打印出以下信息:
这是一个关于测试覆盖率
的说明,这是一个图表信息
,它告诉我们所有的文件都被100%
的测试到了。
如果觉得执行npx jest --coverage
命令太麻烦,你也可以将如下代码添加到 package.json
中:
"scripts": {
"test": "jest",
"coverage": "jest --coverage"
},
复制代码
然后可以执行npm run coverage
,和执行npx jest --coverage
是一样的效果。
除了控制台可以看到图表信息
之外,你还可以在当前目录下看到一个生成的coverage目录
:
在里面有一个lcov-report
文件夹,里面有一个index.html
,我们打开它会看到:
可以看到,这是一个通过Jest
生成的非常漂亮的html页面,上面会告诉我们,我们的每一个文件的测试到底覆盖了多少代码。可以看到,math.js 中的代码都被100%的覆盖到了。
通过这个例子,我们就理解了code coverage(代码覆盖率)
的意思了。
总结来说:测试覆盖率
就是我们编写的测试代码
对原来的功能代码
的占比。
tips: 配置文件中的coverageDirectory
就是代码测试覆盖率生成报告所在的文件夹
的名字:
module.exports = {
coverageDirectory: "abc"
};
复制代码
例如,我们让coverageDirectory
的值为abc
,那么生成的测试报告就在目录abc
下面。
2. 把 commonjs 改成 es module
我们先把 math.js
和 math.test.js
中的代码改成 es module
的规范:
// math.js
export function add(a, b) {
return a + b;
}
export function minus(a, b) {
return a - b;
}
export function multi(a, b) {
return a * b;
}
复制代码
// math.test.js
import { add, minus, multi } from "./math";
test("测试加法 3 + 3", () => {
expect(add(3, 3)).toBe(6);
});
test("测试减法 3 - 3", () => {
expect(minus(3, 3)).toBe(0);
});
test("测试乘法 3 * 3", () => {
expect(multi(3, 3)).toBe(9);
});
复制代码
然后运行 npm run test
,会发现控制台报错了。
要解决这个问题,需要用到 Babel
把代码进行转化就OK了,如果想让 Babel
支持 ESM,我们需要三个包:
@babel/core
:babel核心库@babel/preset-env
:进行 ES 语法转换的库babel-jest
:和 Jest 通信的库,用来检测是否安装了上面两个依赖
但是因为我们在安装 Jest
的时候 babel-jest
就默认被安装了,所以我们只需要安装剩下两个包就行了。
安装依赖:
npm install @babel/core @babel/preset-env -D
复制代码
(注:-D 是 --save -dev 的缩写)
安装完成之后,在项目的根目录下创建一个 .babelrc
配置文件:
创建完成之后,将如下代码添加到 .babelrc
中:
{
"presets": [["@babel/preset-env", { "targets": { "node": "current" } }]]
}
复制代码
保存之后,重新运行 npm run test
,执行测试用例即可。
5.4 Jest 中的匹配器(matchers)
众所周知,在前面的代码中,我们在编写一个测试用例的时候用到了 test
、expect
、toBe
三个方法。
这种语法大概可以翻译为:期待一个值
的结果去匹配另一个值
。
比如:
expect(1 + 1).toBe(2);
复制代码
这个例子可以翻译为:期待 1 + 1
的结果是 2
,这里的 toBe
就是一个匹配器,用来判断接收值
和期望值
是否相等。
事实上,在 Jest
中,除了 toBe
之外,还有很多匹配器
:
toBe
:测试两个对象的值是否相等,类似于 js 中的 === 运算符
// toBe
test("测试加法交换律", () => {
for (let x = 0; x <= 10; x++) {
for (let y = 0; y <= 10; y++) {
expect(x + y).toBe(y + x);
}
}
});
复制代码
toEqual
:测试两个对象的原始值是否相等,只检查内容,不检查引用
// toEqual
const can1 = { value: "hello" };
const can2 = { value: "hello" };
test("测试 can1 和 can2 的内容是否相等", () => {
expect(can1).toEqual(can2);
});
复制代码
toBeNull
:测试对象的值是否为null,效果相当于.toBe(null)
// toBeNull
const value = null;
test("测试值是否为 null", () => {
expect(value).toBeNull();
});
复制代码
toBeUndefined
:测试对象的值是否为undefined,效果相当于.toBe(undefined)
// toBeUndefined
const value = undefined;
test("测试值是否为 undefined", () => {
expect(value).toBeUndefined();
});
复制代码
toBeDefined
:测试值是否被定义过,除了undefined之外都会通过测试
// toBeDefined
const value = 1;
test("测试值是否被定义过", () => {
expect(value).toBeDefined();
});
复制代码
toBeTruthy
:检查值转成布尔值之后是否为真值toBeFalsy
:检查值转成布尔值之后是否为假值
// toBeTruthy、toBeFalsy
test("测试是否为真值", () => {
expect(0).toBeTruthy(); // 不通过
expect("").toBeTruthy(); // 不通过
expect(null).toBeTruthy(); // 不通过
expect(false).toBeTruthy(); // 不通过
expect(undefined).toBeTruthy(); // 不通过
});
test("测试是否为假值", () => {
expect(0).toBeFalsy(); // 通过
expect("").toBeFalsy(); // 通过
expect(null).toBeFalsy(); // 通过
expect(false).toBeFalsy(); // 通过
expect(undefined).toBeFalsy(); // 通过
});
复制代码
not
:取反匹配器,相当于 js 中的 ! 运算符
// not
test("测试值是否不为 aaa", () => {
expect("hello").not.toBe("aaa");
});
test("测试值是否不为 null", () => {
expect([]).not.toBeNull();
});
test("测试值是否不为 undefined", () => {
expect({}).not.toBeUndefined();
});
复制代码
toBeGreaterThan
:检查接收值是否大于期待值
// toBeGreaterThan
test("测试 10 是否大于 9", () => {
expect(10).toBeGreaterThan(9);
});
复制代码
toBeLessThan
:检查接收值是否小于期待值
// toBeLessThan
test("测试 10 是否小于 20", () => {
expect(10).toBeLessThan(20);
});
复制代码
toBeGreaterThanOrEqual
:检查接收值是否大于等于期待值
// toBeGreaterThanOrEqual
test("测试 10 是否大于等于 10", () => {
expect(10).toBeGreaterThanOrEqual(10);
});
复制代码
toBeLessThanOrEqual
:检查接收值是否小于等于期待值
// toBeLessThanOrEqual
test("测试 10 是否小于等于 10", () => {
expect(10).toBeLessThanOrEqual(10);
});
复制代码
toBeCloseTo
:检查浮点数是否接近(是否近似相等)
// toBeCloseTo
test("测试 0.1 + 0.2 是否等于 0.3", () => {
expect(0.1 + 0.2).toBe(0.3); // 不通过
expect(0.1 + 0.2).toBeCloseTo(0.3); //通过
});
复制代码
toMatch
:检查值是否和字符串或者正则相匹配
// toMatch
test("测试字符串是否包含 baidu", () => {
expect("www.baidu.com").toMatch("baidu");
expect("www.baidu.com").toMatch(/baidu/);
});
复制代码
toContain
:检查数组中是否包含某一项(类似于 js 中的 includes 方法)
// toContain
test("测试 list 中是否包含 3", () => {
const list = [1, 2, 3];
expect(list).toContain(3);
});
复制代码
toThrow
:测试函数在调用时是否有异常抛出
// toThrow
const fn1 = () => {
console.log("hello");
};
const fn2 = () => {
throw new Error("this is a new err");
};
test("测试 fn1、fn2 调用时是否有异常", () => {
expect(fn1).toThrow(); // 不通过
expect(fn2).toThrow(); // 通过
});
复制代码
在 Jest
中,除了上面这些列举出来的常用的匹配器
之外,还有很多匹配器为我们提供。不需要记住所有,知道常用的几个即可。如果想了解更多,可以点击这里查看官方文档。
5.5 Jest 命令行工具的使用
进入到 package.json
文件中,在 test
命令行后面加上 --watchAll
参数:
"scripts": {
"test": "jest --watchAll",
"coverage": "jest --coverage"
},
复制代码
这样做的好处是,我们不需要每次都执行 npm run test
,在第一次执行之后,进程会自动监听测试用例的变化,如果测试用例代码发生了变化,会自动执行。
运行npm run test
,会看到如下选项:
可以看到,进入到 Watch 模式中,有很多快捷键,这些快捷键的作用如下:
- 按
f
键:只运行失败的测试用例 - 按
o
键:只运行发生更改的测试用例 - 按
p
键:按文件名regex模式进行筛选 - 按
t
键:按测试名称regex模式进行筛选 - 按
q
键:退出监测模式 - 按
Enter
键:触发测试用例
Jest
中有很多命令行工具
,合理的使用命令行,会让我们的测试变的更加灵活、好用。
5.6 异步代码的测试
要测试异步代码,需要先编写异步代码,安装 axios
,安装好 axios 之后,不管是在node环境
还是浏览器环境
,我们都可以发送请求了:
npm install axios --save
复制代码
首先准备一个接口,这个接口的返回值是一个json对象:
{
"success": true
}
复制代码
然后引入axios,编写一个异步函数,并且导出:
import axios from "axios";
export const getData = () => {
return axios.get("http://www.dell-lee.com/react/api/demo.json");
};
复制代码
在测试异步函数的时候,需要这样写:
方法一:Promises 规范
测试成功的返回值:
import { getData } from "./index";
test("测试 getData 的返回值为 { success: true }", () => {
return getData().then(res => {
expect(res.data).toEqual({ success: true });
});
});
复制代码
在测试成功返回值的时候,需要在.then
里面执行测试用例,而且必须在开头加上return
,返回整个异步代码,否则这个测试是没有意义的。
如果你忽略了这个return
,你的测试将在异步函数返回的 promise 解析之前完成。
测试失败的返回值:
import { getData } from "./index";
test("测试 getData 的返回值包含 404", () => {
return getData().catch(err => {
expect.assertions(1);
expect(err.toString()).toMatch("404");
});
});
复制代码
在测试失败返回值的时候,需要在.catch
里面执行测试用例,但是这样做的话,如果请求发送成功,异步函数走进了.then
回调,.catch
里面的内容不会被执行,相当于这个测试用例没有做任何事情,还是照样能通过测试。
要解决这个问题,需要在前面加上expect.assertions(1);
,用来断言这个测试用例调用了一定数量的expect
。如果调用次数不够,测试用例就不会通过。
方法二:.resolves 匹配器 / .rejects 匹配器
测试成功的返回值:
import { getData } from "./index";
test("测试 getData 的返回值为 { success: true }", () => {
return expect(getData()).resolves.toEqual({ success: true });
});
复制代码
你也可以在 expect
语句中使用.resolves
匹配器,Jest
将等待该 Promises 函数
被 resolve
。
如果 Promises 函数
被 reject
,测试用例将自动失败。
测试失败的返回值:
import { getData } from "./index";
test("测试 getData 的返回值包含 404", () => {
return expect(getData()).rejects.toMatch("404");
});
复制代码
同理,你也可以在 expect
语句中使用.rejects
匹配器,Jest
将等待该 Promises 函数
被 reject
。
如果 Promises 函数
被 resolve
,测试用例将自动失败。
方法三:Async / Await
测试成功的返回值:
import { getData } from "./index";
test("测试 getData 的返回值为 { success: true }", async () => {
const { data } = await getData();
expect(data).toEqual({ success: true });
});
复制代码
你也可以像编写普通的 js 代码那样,给 test
方法的回调函数传递 async
关键字,在 expect
语句前使用await
关键字调用异步函数拿到返回值。
测试失败的返回值:
import { getData } from "./index";
test("测试 getData 的返回值包含 404", async () => {
expect.assertions(1);
try {
await getData();
} catch (err) {
expect(err.message).toMatch("404");
}
});
复制代码
同理,如果需要测试失败的返回值,你需要使用原生 js 中的 try / catch
语句来捕获异常,同时配合 expect.assertions(1);
用来校验这个测试用例调用了一定数量的expect
。如果调用次数不够,说明这里没有捕获到异常,测试用例就不会通过。
方法四:Async、Await 和 .resolves、.rejects 结合使用
import { getData } from "./index";
test("测试 getData 返回成功", async () => {
await expect(getData()).resolves.toEqual({ success: true });
});
test("测试 getData 返回失败", async () => {
await expect(getData()).rejects.toMatch("404");
});
复制代码
在这种情况下,async
和 await
与 promise
示例使用的逻辑是相同的语法糖。
5.7 Jest 中的钩子函数
通常,在编写测试时,你需要在测试运行之前进行一些初始化工作,并且需要在测试运行之后进行一些完成工作。Jest
提供了钩子函数
来处理这个问题。
我们通过一个 计数器
来学习钩子函数
相关的知识。
首先在index.js
里面定义一个类,并且导出:
// index.js
class Counter {
constructor() {
this.number = 0;
}
add() {
this.number++;
}
minus() {
this.number--;
}
}
export default Counter;
复制代码
可以看到,这是一个使用 ES6
语法定义的 class
:
- 在实例化的时候定义了
number
,初始值是0
- 定义了静态方法
add
,执行结果为number
加1
- 定义了静态方法
minus
,执行结果为number
减1
然后在index.test.js
里面引入这个类,测试add
方法:
// index.test.js
import Counter from "./index";
const counter = new Counter();
test("测试 Counter 的 add 方法", () => {
expect(counter.number).toBe(0);
counter.add();
expect(counter.number).toBe(1);
});
复制代码
然后运行测试用例,完美通过。
然后继续添加minus
方法的测试用例:
// index.test.js
import Counter from "./index";
const counter = new Counter();
test("测试 Counter 的 add 方法", () => {
expect(counter.number).toBe(0);
counter.add();
expect(counter.number).toBe(1);
});
test("测试 Counter 的 minus 方法", () => {
expect(counter.number).toBe(0);
counter.minus();
expect(counter.number).toBe(-1);
});
复制代码
然后运行测试用例,结果为:
- 测试
add
方法:通过 - 测试
minus
方法:不通过
出现这个情况的原因是:
我们的实例化过程写在了测试用例外面,所有的测试用例公用一个counter 实例
,所以在测试 add
方法的时候,我们已经对实例的number
属性做出了修改。导致minus
方法的测试用例没用通过。
解决办法是:
我们可以把实例化写在每一个测试用例里面,每次测试
都创建一个新的counter 实例
。这样就不会公用一个counter
了,也不会影响到其它实例了。
但是一般情况下,我们不会
这样做,因为测试用例很多的话,每次都创建一个 counter实例
是一件很麻烦的事情,假如当前文件中有1000
个和counter
有关的测试用例,那么就要创建1000
次counter
实例。
这个时候!钩子函数派上用场了!
// index.test.js
import Counter from "./index";
let counter = null;
beforeEach(() => {
counter = new Counter();
});
test("测试 counter 的 add 方法", () => {
expect(counter.number).toBe(0);
counter.add();
expect(counter.number).toBe(1);
});
test("测试 counter 的 minus 方法", () => {
expect(counter.number).toBe(0);
counter.minus();
expect(counter.number).toBe(-1);
});
复制代码
然后运行测试用例,结果通过。
beforeEach()
的作用是在每个测试用例执行之前执行里面的回调函数,如果你需要在测试开始之前对很多个测试做一些重复的工作,比如要初始化状态,你就可以使用它。
实际上,Jest
一共有四个钩子函数:
beforeAll
:在所有测试用例执行之前调用(调用一次)afterAll
:在所有测试用例执行之后调用(调用一次)beforeEach
:在每个测试用例执行之前调用(调用多次)afterEach
:在每个测试用例执行之后调用(调用多次)
5.8 钩子函数的作用域
在了解作用域之前,需要先了解一个小知识点:Scoping
默认情况下,beforeAll
和afterAll
应用于文件中的每个测试用例。
实际上,还可以使用describe
方法将测试用例进行分组
。当它们位于describe
中时,beforeAll
和afterAll
只应用于当前分组
中的测试用例。
// index.test.js
describe("测试分组1", () => {
beforeAll(() => {
console.log("测试分组1 - beforeAll");
});
afterAll(() => {
console.log("测试分组1 - afterAll");
});
test("测试", () => {
console.log("测试分组1 测试");
expect(1 + 1).toBe(2);
});
});
describe("测试分组2", () => {
beforeAll(() => {
console.log("测试分组2 - beforeAll");
});
afterAll(() => {
console.log("测试分组2 - afterAll");
});
test("测试", () => {
console.log("测试分组2 测试");
expect(1 + 1).toBe(2);
});
});
复制代码
执行上面代码:
在默认情况下,Jest
将按照describe
的顺序连续运行所有测试分组,等待每个测试完成后再继续。
需要注意的是:
- 如果我们不进行分组,相当于在最外面写了一层
describe
。 - 除此之外,实际上,在
describe
里面还能嵌套describe
,就像下面这样:
// index.test.js
describe("第一层", () => {
beforeAll(() => console.log("第一层 - beforeAll"));
describe("第二层", () => {
beforeAll(() => console.log("第二层 - beforeAll"));
describe("第三层", () => {
beforeAll(() => console.log("第三层 - beforeAll"));
test("测试", () => {
console.log("测试");
expect("hello" + " " + "world").toBe("hello world");
});
});
});
});
复制代码
运行这个测试,可以看到下面结果:
所以可以得出一些结论:
- 每一个
describe
都可以有自己的钩子函数
- 每一个
describe
都有自己的作用域
- 每一个
钩子函数
也有自己的作用域,就是当前所在的describe
- 每一个
describe
里面的钩子函数
对自己作用域
下面所有的测试用例
都生效 - 如果
describe
是多层嵌套的,那么测试用例执行的顺序是由外到内
最后再补充一个知识点:
如果有·、多个测试用例,但是只想运行一个的时候,注释掉其它的测试用例往往不是最好的选择,我们可以使.only
语法去执行,:
// index.test.js
test.only("这个测试会被执行", () => {
expect("A").toBe("A");
});
test("这个测试会被跳过", () => {
expect("B").toBe("B");
});
复制代码
这样就可以单独运行
一个测试用例,其它测试会被跳过
。
5.9 Jest 中的 Mock
1. 函数的 Mock
在我们的项目中,一个模块的方法内通常会去调用另外一个模块的方法。
在测试时,我们可能并不需要关心方法内部的执行过程和结果,只想知道它是否被正确调用即可,此时,就需要用到Jest
的使用Mock
函数了。
Mock
函数提供的以下三种特性,在我们写测试代码时十分有用:
- 捕获函数调用情况
- 设置函数返回值
- 改变函数的内部实现
1.1 测试函数是否被正常调用
首先定义一个函数,用来执行传入的回调,然后导出:
// index.js
export const runCallback = callback => {
callback();
};
复制代码
然后我们需要这样测试:
// index.test.js
import { runCallback } from "./index";
test("测试 runCallback", () => {
const func = jest.fn(); // 生成 mock 函数,捕获函数的调用
runCallback(func); // 调用 mock 函数
expect(func).toBeCalled(); // toBeCalled 匹配器用来检查函数是否被调用过
});
复制代码
1.2 测试函数调用次数是否正确
首先定义一个函数,用来执行传入的回调,然后导出:
// index.js
export const runCallback = callback => {
callback();
};
复制代码
然后我们需要这样测试:
// index.test.js
import { runCallback } from "./index";
test("测试调用次数", () => {
const func = jest.fn(); // 生成 mock 函数,捕获函数的调用
runCallback(func); // 第一次调用 mock 函数
runCallback(func); // 第二次调用 mock 函数
runCallback(func); // 第三次调用 mock 函数
expect(func.mock.calls.length).toBe(3); // 检查函数是否被调用了三次
});
复制代码
1.3 测试函数是否返回 undefined
首先定义一个函数,用来执行传入的回调,然后导出:
// index.js
export const runCallback = callback => {
callback();
};
复制代码
然后我们需要这样测试:
// index.test.js
import { runCallback } from "./index";
test("测试返回值", () => {
const func = jest.fn(); // 生成 mock 函数,捕获函数的调用
expect(runCallback(func)).toBeUndefined(); // 检查函数是否返回 undefined
});
复制代码
六、章节总结
这篇文章讲了jest相关的一些基础内容,如有错误,敬请指正。
大家可以参阅官方文档进行学习jest,在学习这部分基础内容之后,你的jest就算入门了。
后面如果有时间,我还会出一篇jest高级用法的文章,比如snapshot 快照测试
、mock timers
、ES6中类的测试
、对 DOM 节点的测试
。