Jest基础实践
官方文档地址:https://jest.nodejs.cn/docs
生命周期
在 Jest
中,生命周期方法大致分为两类:下面所罗列的生命周期方法,也是全局方法,不需要引入,直接就可以使用。
- 重复性的生命周期方法:会在每一个测试用例的前后执行
- beforeEach
- afterEach
- 一次性的生命周期方法:在整个测试开始之前,以及测试用例全部执行完之后执行该生命周期方法。
- beforeAll
- afterAll
beforeEach和afterEach
const { sum, sub } = require('../utils/computend')
// beforeEach 会在执行每一个测试用例之前被触发
beforeEach(() => {
console.log('beforeEach')
})
// afterEach 会在执行每一个测试用例之后被触发
afterEach(() => {
console.log('afterEach')
})
test('sum:测试加法是否正确', () => {
console.log('test')
// 断言 1+2=3
expect(sum(1, 2)).toBe(3)
// 断言 1+2 != 4
expect(sum(1, 2)).not.toBe(4)
})
it('sub:测试减法是否正确', () => {
console.log('it')
// 断言 2-1=1
expect(sub(2, 1)).toBe(1)
// 断言 2-1 != 2
expect(sub(2, 1)).not.toBe(2)
})
上面代码console输出顺序是:
console.log('beforeEach')
console.log('test')
console.log('afterEach')
console.log('beforeEach')
console.log('it')
console.log('afterEach')
beforeAll和afterAll
// beforeAll 是在整个测试套件的第一个测试用例执行之前执行
beforeAll(()=>{
console.log("全局的beforeAll");
})
// afterAll 会在所有测试用例执行完成之后,然后再执行 afterAll
afterAll(()=>{
console.log("全局的afterAll");
})
test('sum:测试加法是否正确', () => {
console.log('test')
// 断言 1+2=3
expect(sum(1, 2)).toBe(3)
// 断言 1+2 != 4
expect(sum(1, 2)).not.toBe(4)
})
it('sub:测试减法是否正确', () => {
console.log('it')
// 断言 2-1=1
expect(sub(2, 1)).toBe(1)
// 断言 2-1 != 2
expect(sub(2, 1)).not.toBe(2)
})
输出:
全局的beforeAll
test
it
全局的afterAll
在分组中添加生命周期函数
如果测试用例比较多,我们可以使用 describe 来进行分组,在一个分组里面也可以书写生命周期方法,但是在分组中的生命周期方法会变为一个局部的生命周期方法,仅对该组测试用例有效。
describe('测试加法', () => {
beforeEach(() => {
console.log('beforeEach')
})
test('1+2=3', () => {
console.log('test')
expect(sum(1, 2)).toBe(3)
})
test('1+2 != 4', () => {
console.log('it')
expect(sum(1, 2)).not.toBe(4)
})
})
输出:
beforeEach
test
it
但这里还涉及到了一个顺序的问题:如果既有全局的 beforeEach 又有分组内部的 beforeEach,那么是先执行全局的 beforeEach,然后再执行分组内部的 beforeEach,如果是全局 afterEach 以及 分组的 afterEach,那么顺序刚好和 beforeEach 相反。
beforeEach(() => {
console.log('全局的beforeEach')
})
afterEach(() => {
console.log('全局的afterEach')
})
describe('测试加法', () => {
beforeEach(() => {
console.log('内部的beforeEach')
})
afterEach(() => {
console.log('内部的afterEach')
})
test('1+2=3', () => {
console.log('test')
expect(sum(1, 2)).toBe(3)
})
test('1+2 != 4', () => {
console.log('it')
expect(sum(1, 2)).not.toBe(4)
})
})
/*
输出顺序:
全局的beforeEach
内部的beforeEach
内部的afterEach
全局的afterEach
*/
同样我们也可以添加分组内部的 beforeAll 和 afterAll,beforeAll 是在要执行该分组的测试用例之前会执行,afterAll 是在该分组所有测试用例执行完毕后执行。
beforeEach(() => {
console.log('全局的beforeEach')
})
afterEach(() => {
console.log('全局的afterEach')
})
describe('测试加法', () => {
beforeAll(() => {
console.log('内部的beforeAll')
})
afterAll(() => {
console.log('内部的afterAll')
})
beforeEach(() => {
console.log('内部的beforeEach')
})
afterEach(() => {
console.log('内部的afterEach')
})
test('1+2=3', () => {
console.log('test')
expect(sum(1, 2)).toBe(3)
})
test('1+2 != 4', () => {
console.log('it')
expect(sum(1, 2)).not.toBe(4)
})
})
/*
输出顺序:
内部的beforeAll
全局的beforeEach
内部的beforeEach
内部的afterEach
全局的afterEach
内部的afterAll
*/
test.only
test.only是用来测试特定的测试用例,也就是说,如果一个测试套件里面假设有10个测试用例,第7个测试用例书写了 test.only,那么在运行整个测试套件的时候,就只会执行第 7 个测试用例。
test.only("测试乘法函数", () => {
const result = mul(2, 3);
expect(result).toBe(6);
console.log("\x1b[31m%s\x1b[0m", "测试乘法函数");
});
使用 test.only 可以很方便地运行单个测试用例,以便在调试失败的测试用例时进行测试
模拟函数
jest 对象上面的方法大致分为四类:
- 模拟模块
- 模拟函数
- 模拟计时器
- 其他方法
模拟函数
语法:jest.fn(implementation?);implementation 是一个可选参数,代表着模拟函数的实现,如果没有传入,那么创建的是一个空的模拟函数。
// 示例1
test('创建一个模拟函数', () => {
const mockFunction = jest.fn();
// 设置这个模拟函数的返回值为 42
mockFunction.mockReturnValue(42);
// mockFunction()函数或者toBe()里面必须是42才对,其他数字是错;要一一对应
expect(mockFunction()).toBe(42);
})
// 示例2
test('mock function', () => {
const mockCallback = jest.fn(x => 42 + x);
expect(mockCallback(1)).toBe(43);
})
通过模拟函数身上的这些方法,可以控制模拟函数的行为,例如下面我们通过 mockReturnValueOnce 控制函数不同次数的调用对应的返回值
test("基本演示",()=>{
// 创建一个模拟函数
const mock = jest.fn();
mock.mockReturnValue(30) // 设置返回值为 30
.mockReturnValueOnce(10) // 第一次调用模拟函数对应的返回值
.mockReturnValueOnce(20) // 第二次调用模拟函数对应的返回值
expect(mock()).toBe(10);
expect(mock()).toBe(20);
expect(mock()).toBe(30);
// 设置这个模拟函数的返回值为 42
mock.mockReturnValue(42);
expect(mock()).toBe(42);
});
**toHaveLength:**用于验证对象的长度是否等于指定的值
mock.calls: 包含对此模拟函数进行的所有调用的调用参数的数组。数组中的每一项都是调用期间传递的参数数组。
mock.results:包含对此模拟函数进行的所有调用的结果的数组。该数组中的每个条目都是一个包含 type
属性和 value
属性的对象。
toHaveBeenCalled: 确保调用了模拟函数
toHaveBeenCalledWith:使用该方法确保使用特定参数调用模拟函数。使用 .toEqual
使用的相同算法检查参数
toHaveBeenLastCalledWith:如果你有模拟函数,则可以使用该函数来测试上次调用它时使用的参数。
test('测试forEach是否正确', () => {
// 由于 forEach 中依赖了 callback,因此我们可以创建一个模拟函数来模拟这个 callback
const mockCallback = jest.fn(x => 100 + x)
// forEach数据类型是[[1], [2], [3]]
forEach(arr, mockCallback)
// toHaveLength: 验证对象的长度是否等于指定的值
expect(mockCallback.mock.calls).toHaveLength(3);
expect(mockCallback.mock.calls.length).toBe(3);
// 测试每一次调用 callback 的时候传入的参数是否符合预期
expect(mockCallback.mock.calls[0][0]).toBe(1);
expect(mockCallback.mock.calls[1][0]).toBe(2);
expect(mockCallback.mock.calls[2][0]).toBe(3);
// 针对每一次 callback 被调用后的返回值进行测试
expect(mockCallback.mock.results[0].value).toBe(101);
expect(mockCallback.mock.results[1].value).toBe(102);
expect(mockCallback.mock.results[2].value).toBe(103);
// 模拟函数是否被调用过
expect(mockCallback).toHaveBeenCalled();
// 前面在调用的时候是否有参数为 1 以及参数为 2 的调用
expect(mockCallback).toHaveBeenCalledWith(1);
expect(mockCallback).toHaveBeenCalledWith(2);
// 还可以对模拟函数的参数进行一个边界判断,判断最后一次调用是否传入的参数为 3
expect(mockCallback).toHaveBeenLastCalledWith(3);
})
模拟请求
在测试异步函数的时候,会发送真实的请求进行测试,但是有一些时候,我们知道这个没问题,或者说想要在那时屏蔽这一个异步,假设一个异步是能够正常返回数据的,这种情况下我们就可以针对这个异步请求函数来书写一个模拟函数来代替真实的 异步函数。
mockImplementationOnce:接受一个函数,该函数将用作模拟函数的一次调用的模拟实现。可以链接起来,以便多个函数调用产生不同的结果。
示例:
const mockFn = jest.fn()
.mockImplementationOnce(cb => cb(null, true))
.mockImplementationOnce(cb => cb(null, false));
mockFn((err, val) => console.log(val)); // true
mockFn((err, val) => console.log(val)); // false
rejects:验证一个 Promise 是否被 reject
resolves: 验证一个 Promise 是否被 resolve
模拟请求示例
// 创建了一个空的模拟函数
const fetchDataMock = jest.fn();
const fakeData = { id: 1, name: "xiejie" };
// 设置该模拟函数的实现
fetchDataMock.mockImplementation(() => Promise.resolve(fakeData));
// 通过模拟函数的一些方法来设置该模拟函数的行为
test("模拟网络请求正常", async () => {
const data = await fetchDataMock();
expect(data).toEqual({ id: 1, name: "xiejie" });
});
test("模拟网络请求出错", async () => {
// 模拟网络请求第一次请求失败,之后请求没问题
fetchDataMock.mockImplementationOnce(() =>
Promise.reject(new Error("network error"))
);
// rejects: 验证一个 Promise 是否被 reject; toThrow: 验证一个 Promise 是否被 reject,并且抛出了指定的错误
await expect(fetchDataMock()).rejects.toThrow("network error");
// resolves: 验证一个 Promise 是否被 resolve; toEqual: 验证一个 Promise 是否被 resolve,并且返回的 Promise 的值与指定的值相等
await expect(fetchDataMock()).resolves.toEqual({ id: 1, name: "xiejie" });
});
模拟模块
模块可以分为两种模块:第三方模块和文件模块
mock(): 测试时可以轻松替换和控制外部依赖的行为,让你能够专注于测试代码的逻辑,而不是依赖于外部系统。
模拟第三方模块
下面的代码的目的是通过使用 Jest 来对一个与 axios
相关的 User
类进行单元测试,模拟了 axios
模块的行为并验证了 User
类中的方法是否按预期工作。
api/userApi.js
// 这个文件是模拟axios请求的
const axios = require("axios");
class User {
// 获取所有的用户
static all() {
return axios.get("/users.json").then((resp) => resp.data);
}
static testArg(){
// 这个方法本身 axios 是没有的
// 我们通过模拟 axios 这个模块,然后给 axios 这个模块添加了这么一个 test方法
// 这里在实际开发中没有太大意义,仅做演示
return axios.test();
}
}
module.exports = User;
mock/user.json
// 模拟后端返回的数据
[
{
"name": "张三",
"age": 18,
"gneder": "男",
"score": 100
},
{
"name": "李四",
"age": 20,
"gneder": "男",
"score": 99
}
]
mock.test.js
// 写测试用例的文件
const User = require('../api/userApi')
const userData = require('../mock/user.json')
// 模拟 axios 模块
// jest.mock() 是 Jest 提供的功能,用于模拟 axios 模块的行为
jest.mock('axios', () => {
const userData = require('../mock/user.json')
// 模拟响应数据
const resp = {
data: userData
}
return {
get: jest.fn(() => Promise.resolve(resp)),
test: jest.fn(() => Promise.resolve("this is a test"))
}
})
test('测试用例', async () => {
// User.all()方法能够正确地通过模拟的 axios.get 获取到预期的用户数据。
await expect(User.all()).resolves.toEqual(userData);
// 除了替换模块本身,还可以为这个模块添加一些额外的方法:
// User.testArg() 方法能够正确地调用模拟的 axios.test 方法,返回预期的字符串 "this is a test"。
await expect(User.testArg()).resolves.toEqual("this is a test");
})
模拟文件模块
通过 jest.mock,我们还可以模拟整个文件模块
**requireActual:**作用是返回实际模块而不是模拟,绕过有关模块是否应接收模拟实现的所有检查。
const { sum, sub, mul, div } = require("../utils/computend");
jest.mock("../utils/computend", () => {
// 在这里来改写文件模块的实现
// 拿到 ../utils/computend 路径所对应的文件原始模块
const originalModule = jest.requireActual("../utils/computend");
// 这里相当于是替换了原始的模块
// 一部分方法使用原始模块中的方法
// 一部分方法(sum、sub)被替换掉了
return {
...originalModule,
sum: jest.fn(() => 100),
sub: jest.fn(() => 50),
};
});
test("对模块进行测试", () => {
expect(sum(1, 2)).toBe(100);
expect(sub(10, 3)).toBe(50);
expect(mul(10, 3)).toBe(30);
expect(div(10, 2)).toBe(5);
});
可以看到在运行的时候,没有再像之前一样显示出测试用例的描述。如果想要显示,可以添加如下的配置:
// package.json的scripts里面添加如下命令
{
"test": "jest --verbose=true"
}
// 需要统一运行,运行单个文件没用
配置文件
生成文件
npx jest --init // 生成测试文件
会需要选择如下命令
Would you like to use Jest when running "test" script in "package.json"? no/yes
Would you like to use Typescript for the configuration file? no/yes
Choose the test environment that will be used for testing ? node/jsdom
Do you want Jest to add coverage reports? no/yes
Which provider should be used to instrument code for coverage? v8/babel
Automatically clear mock calls, instances, contexts and results before every test? no/yes
上面的翻译结果是:
您想在“package.json”中运行“测试”脚本时使用Jest吗?no/yes --选择yes
您想要使用Typescript作为配置文件吗?no/yes --根据你项目是ts还是js来选择
选择将用于测试的测试环境?node/ jsdom --选择node
您希望Jest添加覆盖率报告吗?no/yes --选择yes
应该使用哪个提供者来为覆盖率编写代码?v8 /babel --选择v8
在每次测试前自动清除模拟调用、实例、上下文和结果?no/yes --选择yes
生成jest.config.js后,运行命令生成对应测试结果和根目录的coverage文件夹
// cmd界面运行命令会输出
npm run test
生成对应的表格完成率
- % Stmts:包含语句的百分比,即被测试覆盖的语句占总语句数的比例。
- % Branch:包含分支的百分比,即被测试覆盖的分支占总分支数的比例。
- % Funcs:包含函数的百分比,即被测试覆盖的函数占总函数数的比例。
- % Lines:包含行的百分比,即被测试覆盖的行占总行数的比例。
- Uncovered Line #s:未被测试覆盖的行号。
项目根目录下面,还新生成了一个 coverage 的目录,里面其实就是各种格式(xml、json、html)的测试报告,之所以生成不同格式的报告,是为了方便你后面通过不同的工具来进行读取。也可以打开根目录coverage/lcov-report/index.html,可以浏览器查看
文件解析
介绍一些jest.config.js配置文件中常见的配置项属性含义:
**coverageProvider:**收集并显示测试覆盖率,包含每个文件中每种类型的代码(语句、分支、函数和行)的测试覆盖率
**coverageThreshold:**当设置collectCoverage为true之后,就可以设置coverageThreshold代码覆盖率的阀值,用于指定每个文件、目录或整个项目的最低覆盖率百分比。
coverageThreshold: {
global: { // global是全局
branches: 90, // 分支覆盖率的目标百分比。
functions: 90, // 函数覆盖率的目标百分比。
lines: 90, // 行覆盖率的目标百分比。
statements: 90, // 语句覆盖率的目标百分比。
},
'./src/utils/': { // 设定特定文件的阀值
branches: 60,
functions: 60,
lines: 60,
statements: 60
}
}
**testMatch:**这个配置项可以指定 Jest 应该运行哪些测试文件。默认情况下, Jest 会查找 .test.js 或者 .spec.js 结尾的文件
testMatch: [
"**/test/**/*.[jt]s?(x)", //指定解析的文件格式
],
**moduleFileExtensions 😗*指定 Jest 查找测试文件时应该搜索哪些文件扩展名。
setupFilesAfterEnv:指定 Jest 在运行测试之前应该运行哪些文件。例如:
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js']
rootDir: 是获取当前根目录
在执行每个测试套件(文件)之前,都会先执行这个 setupTests 文件