Jest 指南
Jest 是 Facebook 开源的基于 Bettery-included 单元测试解决方案的一个流行的 JavaScript 测试框架,被广泛应用于前端的单元测试、集成测试和端到端测试。它具有简单易用、快速、可靠、可扩展等特点,适用于各种类型的前端项目的测试。
基本概念
测试用例 测试用例是指用于测试代码的一组输入和输出数据,用于检验代码是否满足预期的功能和行为。在 Jest 中,测试用例通常由一个或多个测试断言组成,用于判断代码的输出是否符合预期。
测试断言 测试断言是指用于判断代码输出是否符合预期的语句,通常由 Jest 提供的断言库或自定义的断言函数组成。在 Jest 中,常用的测试断言包括 expect、toBe、toEqual、toMatch 等。
测试用例组 测试用例组是指包含多个测试用例的测试集合,通常用于对某个模块、组件或功能进行全面的测试。在 Jest 中,测试套件通常使用 describe 函数来定义,可以嵌套多个 describe 函数来组织测试用例。
使用方法
安装 Jest
在使用 Jest 之前,需要先安装 Jest 和相关的依赖包。可以通过以下命令来安装:
npm i -D jest
yarn add -D jest
pnpm i -D jest注:由于安装的 jest 默认是最新版本,可能在老项目中与 babel 包或者其他依赖版本不对应,安装时要确定合适的 jest 版本。 比如 babel-core: 6.18.2,此时应该安装 jest:20.0.4 版本才能正常运行。
安装依赖后,配置 package.json:
{
"scripts": {
"test": "jest"
}
}编写示例
安装完成后,可以在项目中创建一个简单的测试文件 test.js,并编写一个简单的测试用例:
test('adds 1 + 2 to equal 3', () => {
expect(1 + 2).toBe(3);
});执行用例
然后,在命令行中执行 npm run test 命令,默认匹配项目以 .test.js 或者 .spec.js 文件,运行测试用例并输出测试结果。
PASS src\js\test.js (5.54s)
FAIL test\util\__test__\eventTracking.test.js (5.56s)
● eventTracking › should not check chrome ua
ReferenceError: checkWxBrowser is not defined
at Object.<anonymous> (test/util/__test__/eventTracking.test.js:4:9)
at new Promise (<anonymous>)
at node_modules/p-map/index.js:46:16
at processTicksAndRejections (node:internal/process/task_queues:96:5)
PASS test\util\__test__\ua.test.js (5.564s)
Test Suites: 1 failed, 2 passed, 3 total
Tests: 3 failed, 4 passed, 7 total
Snapshots: 0 total
Time: 16.262s
Ran all test suites.Test Suites: 表示共3个用例组测试结果 Tests: 测试用例执行结果 如果要执行某个文件的测试用例,可以同过命令 npm run test -- [文件名] 执行当前文件的测试用例,比如 npm run test -- ua.test.js。
Jest eslint配置
如果配置eslint项目,jest要新增额外配置,不然编译局对jest语法会报红。
1、新增 test/.eslintrc.cjs 文件
module.exports = {
globals: {
describe: 'readonly',
expect: 'readonly',
it: 'readonly',
xdescribe: 'readonly',
xit: 'readonly'
},
overrides: [
{
files: ['**/*.{ts}'],
rules: {
'import/no-unresolved': 'off',
'import/no-extraneous-dependencies': 'off',
},
},
],
};2、项目 eslint 增加配置
"env": {
"jest": true,
}注:以上是使用 Jest 的基础文档,如果要在成熟的前端工程接入 Jest,请阅读接入示例文档,不同的前端工程接入 Jest 方式和依赖版本可能不尽相同。
生命周期钩子函数
1、beforeAll 所有测试用例执行之前执行,只执行一次。
2、beforeEach 每个测试用例执行之前都会执行,可能执行多次。
3、afterEach 每个测试用例执行之后都会执行,可能执行多次。
4、afterAll 所有测试用例执行之后执行,只执行一次。
比如在 dayjs 中,使用 beforeEach、afterEach 设置和清除当前时间:
beforeEach(() => {
MockDate.set(new Date())
})
afterEach(() => {
MockDate.reset()
})全局方法
describe(name, fn)
describe(name, fn) 用例组,用于创建一个代码块,将多个相关的测试用例组合在一起,名称用于描述测试用例的主题,通常是一个字符串。函数用于编写测试用例,可以包含一个或多个 test 方法:
describe('add', () => {
it('should add two numbers', () => {
expect(add(6, 4)).toBe(10);
expect(add(-6, 4)).toBe(-2);
expect(add(-6, -4)).toBe(-10);
});
it('should not coerce arguments to numbers', () => {
expect(add('6', '4')).toBe('64');
expect(add('x', 'y')).toBe('xy');
});
});test(name, fn, timeout)
test 方法用于编写单个测试用例,有多少个test就有多少个测试用例,名称用于描述测试用例的目的,通常是一个字符串。函数用于编写测试用例的代码,包括一些操作和断言。
it('should return `true` for arrays', () => {
expect(isArray([1, 2, 3])).toBe(true);
});断言 Expect
测试断言,编写测试时,经常需要检查值是否满足某些条件, expect 可以访问许多 matchers,验证不同的测试结果。
expect(1 + 2).toBe(3);匹配器 Matcher
toBe(value)
检查一个值是否等于另一个值,使用 Object.is 进行相等比较。 console.log(Object.is(NaN, NaN)); // true、console.log(Object.is(+0, -0)); // false
expect(isArray([1,2,3])).toBe(true);
expect(isArray(true)).toBe(false);
expect(isArray(new Date())).toBe(false);
expect(isArray(new Error())).toBe(false);
expect(isArray(slice)).toBe(false);
expect(isArray({ 0: 1, length: 1 })).toBe(false);
expect(isArray(1)).toBe(false);
expect(isArray(/x/)).toBe(false);
expect(isArray('a')).toBe(false);
expect(isArray(symbol)).toBe(false);toEqual(value)
检查一个值是否深度等于另一个值,递归比较对象的属性和数组的元素。
const can1 = {
flavor: 'grapefruit',
ounces: 12,
};
const can2 = {
flavor: 'grapefruit',
ounces: 12,
};
describe('the La Croix cans on my desk', () => {
test('have all the same properties', () => {
expect(can1).toEqual(can2);
});
test('are not the exact same can', () => {
expect(can1).not.toBe(can2);
});
});toBeDefined()
检查一个值是否不是 undefined。
toBeNull()
检查一个值是否为 null。
toBeTruthy()
检查一个值是否为真值(即不是 false、0、''、null、undefined 或 NaN)。
toBeFalsy()
检查一个值是否为假值(即 false、0、''、null、undefined 或 NaN)。
toBeGreaterThan(value)
检查一个值是否大于另一个值。
toBeGreaterThanOrEqual(value)
检查一个值是否大于或等于另一个值。
toBeLessThan(value)
检查一个值是否小于另一个值。
toBeLessThanOrEqual(value)
检查一个值是否小于或等于另一个值。
toContain(item)
检查某个项目否在数组中,为了测试数组中的项,使用了 === 严格的相等检查。还可以检查一个字符串是否是另一个字符串的子串。
describe('jest test toContain', () => {
test('toContain { a: 1 } in [{ a: 1 }]', () => {
const obj = { a: 1 };
expect([obj]).toContain(obj);
});
test('toContain abc in abc123', () => {
expect('abc123').toContain('abc');
});
test('toContain 1 in [1, 2, 3]', () => {
expect([1, 2, 3]).toContain(1);
expect([1, 2, 3]).not.toContain('1');
});
test('toContain \'1\' not in [1, 2, 3]', () => {
expect([1, 2, 3]).not.toContain('1');
});
});toHaveLength(length)
检查一个数组或字符串是否具有特定长度。
describe('jest test toHaveLength', () => {
test('toHaveLength [1, 2, 3] length is 3', () => {
expect([1, 2, 3]).toHaveLength(3);
});
});toMatch(regexp)
检查一个字符串是否与正则表达式匹配。
describe('jest test toMatch', () => {
test('toMatch [\'1\', \'a\', \'b\', \'c\'] is matched /1abc/', () => {
const str = (arr) => {
return arr.join('');
};
expect(str(['1', 'a', 'b', 'c'])).toMatch(/1abc/);
});
});toThrow(error?)
检查一个函数是否抛出错误,可选地检查错误消息或类型是否与特定值匹配。
describe('jest test toThrow', () => {
test('toThrow empty error', () => {
const fn = () => {
throw new Error;
};
expect(fn).toThrow();
});
test('toThrow fail error', () => {
const fn = () => {
throw new Error('fail');
};
expect(fn).toThrow('fail');
});
});修饰符
.not
表示取反。它可以用于将一个 Matcher 的结果取反,以检查某些条件是否不成立。
test('adds 1 + 2 to equal 3', () => {
expect(add(1, 3)).not.toBe(3);
});.resolves、.rejects
在 Jest 中,resolves 和 rejects 是用于测试异步代码的执行结果的 Matcher 修饰符。
resolves 用于测试 Promise 对象是否成功地被 resolved,而 rejects 用于测试 Promise 对象是否被 rejected。这两个 Matcher 可以与 expect 函数一起使用,以测试异步代码的执行结果。
describe('jest test Promise', () => {
function fetchData(success) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (success) {
resolve('Success');
} else {
reject(new Error('Failed'));
}
}, 1000);
});
}
test('fetchData resolves with data', () => {
expect(fetchData(true)).resolves.toBe('Success');
});
test('fetchData rejects with an error', () => {
expect(fetchData(false)).rejects.toThrow('Failed');
});
});异步代码测试
在实际开发中,很多代码都是异步执行的,例如通过 AJAX 请求获取数据、通过 Promise 处理异步操作等。在 Jest 中,可以使用 async/await 或 Promise 来编写异步测试用例。
例如,可以编写一个异步测试用例,测试异步函数的返回结果是否符合预期:
Promises
test('the data is success', () => {
return fetchData({id: 1}).then(res => {
expect(res).toEqual({ code: 0, data: 1 });
});
});Async/Await
test('the data is success', async () => {
const res= await fetchData({id: 1});
expect(res).toEqual({ code: 0, data: 1 });
});Mock 测试
在实际开发中,经常会碰见A模块掉B模块的方法,比如很多代码都依赖于外部资源或第三方库,或者调用后端 API 接口、操作数据库、发送邮件等。并且,在单元测试中,我们可能并不需要关心内部调用的方法的执行过程和结果,只想知道它是否被正确调用即可,甚至会指定该函数的返回值。此时,就需要使用 Mock 测试来模拟这些外部函数,以便更方便地编写和运行测试用例。
jest.fn
模拟函数,用于在测试过程中模拟一个函数的行为。它返回一个模拟函数对象,可以通过设置其返回值或者记录其调用情况来模拟一个外部函数的行为。省略了函数内部实现,可以通过调用其他 mock 方法修改函数的行为。
假设我们要测试函数 forEach 的内部实现,这个函数为传入的数组中的每个元素调用一个回调函数,代码如下:
// index.js
export const forEach = (items, callback) => {
for (let index = 0; index < items.length; index++) {
callback(items[index]);
}
}测试这个函数时,我们不关心callback的内部逻辑,只想知道在forEach函数执行时它有没有被调用,所以在编写测试用例时,我们可以使用一个mock函数来代替callback,然后通过检查 mock 函数来确保回调函数是否如期调用。
// index.test.js
import { forEach } from './index.js'
test('mock fn calls', () => {
const mockFn = jest.fn();
forEach([1, 2, 3], mockFn);
expect(mockFn.mock.calls.length).toBe(3);
expect(mockFn.mock.calls[0][0]).toBe(1);
expect(mockFn.mock.calls[1][0]).toBe(2);
});jest.Mock
用于在测试过程中模拟一些外部依赖或者其他模块的行为。比如 fetch.js 文件夹中封装的请求方法可能我们在其他模块被调用的时候,并不需要进行实际的请求(请求方法已经通过单侧或需要该方法返回非真实数据)。此时,使用 jest.mock()去 mock 整个模块是十分有必要的。
// 假设有一个外部模块 utils,用于计算两个数的和
// utils.js
export function add(a, b) {
return a + b;
}
// 在测试中,使用 jest.mock() 创建一个模拟模块对象
jest.mock('./utils');
// 设置模拟模块的返回值
import { add } from './utils';
add.mockReturnValue(3);
// 调用被测代码,期望其调用 add 函数并返回 3
const result = add();
// 检查模拟模块是否被调用
expect(add).toHaveBeenCalled();
// 检查被测代码的返回值是否正确
expect(result).toBe(3);Mock 返回值
mockReturnValue 每当调用修改模拟函数时将返回的值。
const mock = jest.fn();
mock.mockReturnValue(42);
mock(); // 42
mock.mockReturnValue(43);
mock(); // 43mockReturnValueOnce 修改模拟函数的一次调用中的返回值。可以链式调用,以便对模拟函数的连续调用返回不同的值。
const mockFn = jest
.fn()
.mockReturnValue('default')
.mockReturnValueOnce('first call')
.mockReturnValueOnce('second call');
mockFn(); // 'first call'
mockFn(); // 'second call'
mockFn(); // 'default'
mockFn(); // 'default'
mockResolvedValue
mockResolvedValueOnce
mockRejectedValue
mockRejectedValueOnce
返回 resolve 或者 reject 状态的 Promise 函数
示例
```js
test('返回固定值 ', () => {
const func = jest.fn();
func.mockReturnValue('Mocked Return');
func.mockReturnValueOnce('Mocked Return by Once');
expect(func()).toBe('Mocked Return by Once');
expect(func()).toBe('Mocked Return');
});
test('返回Promise对象', async () => {
const func = jest.fn().mockResolvedValue('Mocked Promise');
let res = await func();
expect(res).toBe('Mocked Promise'); // func通过await关键字执行后返回值为Mocked Promise
});jest.fn()如果没有定义函数内部的实现, 默认情况下会返回 undefined :
test('默认返回undefined', () => {
const fun = jest.fn();
const res = fun(1, 2, 3);
expect(res).toBeUndefined(); // 返回undefined
expect(fun).toBeCalledWith(1, 2, 3); // 传入的参数为 1, 2, 3
});模拟内部实现
jest.fn()可以传入一个函数,从而生成带逻辑的函数:
test('模拟内部实现', () => {
const fun = jest.fn();
fun.mockImplementation((num1, num2) => {
return num1 + num2;
});
fun.mockImplementationOnce((num1, num2) => {
return num1 * num2;
});
expect(fun(3, 5)).toBe(15); // 执行mockImplementationOnce
expect(fun(3, 5)).toBe(8); // 执行mockImplementation
});调用生成的mock函数的 mockImplementation 或者 mockImplementationOnce 方法可以改变mock函数的内容,两者的区别是:mockImplementationOnce 方法会在第一次调用时被执行,它可以链式调用,从而每次执行不同的逻辑。
当需要多个函数调用产生不同的结果时,使用 mockImplementationOnce 方法会很有用。
模拟axios
实际测试异步函数的时候,我们不会真正的发送ajax请求去请求这个接口,最好的方式还是mock数据,让它不用发送请求也能测试我们的接口调用是否正确。
比如查询用户信息的请求接口:
// user.js
import axios from 'axios';
export const getUserInfo = () => {
return axios.get('/api/user/info').then(res => res.data);
};
// user.spec.js
import axios from 'axios';
import { getUserInfo } from './user.js';
jest.mock('axios');
test('模拟axios', async () => {
axios.get.mockResolvedValue({
data: {
name: '张三',
age: 20
},
code: 0
});
await getUserInfo().then(data => {
expect(data).toEqual({
name: '张三',
age: 20
}});
});
});jest.mock('axios')模拟了axios模块,并且我们自定义了请求数据,从而将异步获取数据转变为同步准备数据,避免了向后台去请求接口。 注意:jest.mock('axios') 必须写在最外层。
测试覆盖率
在实际开发中,测试覆盖率是衡量测试质量的一个重要指标,可以帮助开发者评估测试用例的覆盖范围和质量。在 Jest 中,可以通过执行下面 npm 脚本生成测试覆盖率 。
"scripts": {
"test:cov": "jest --coverage"
}也可以配置自动收集测试覆盖率报告:
module.exports = {
collectCoverage: true,
collectCoverageFrom: ['src/**/*.js'],
coverageReporters: ['text', 'html'],
};常见问题
1、通过 npm i jest -D 安装 jest 后,单测用例运行失败。
检查 babel-core 依赖版本,确认并重新安装与之匹配的jest版本。
2、测试模拟axios中的示例,出现 axios.get.mockResolvedValue 报错。
因为jest版本过低,不支持当前api,升级jest版本。