Jest 学习指南
Jest 学习指南
# Jest 简介
# 什么是 Jest
Jest 是一个令人愉快的 JavaScript 测试框架,专注于简洁明快。
他适用但不局限于使用以下技术的项目:Babel, TypeScript, Node, React, Angular, Vue
Jest 本体主要专注于单元测试
# 为什么要使用 Jest
对于代码的类型正确性检查,我们可以借助 TypeScript,不需要通过 Jest,
而对于代码实现的具体业务逻辑,我们可以使用 Jest 进行测试和保证。
Jest 包括一些方便的特性:
- 零配置
- 快照功能
- Mock 功能
- 代码覆盖率
- 并行化
在 2018 年 JavaScript 明星项目 (opens new window) 的排名中,Jest 位于测试分类的第一名,
而在 2019 年 JavaScript 明星项目 (opens new window) 中也位于第三
(前两名的 Puppeteer (opens new window) 和 cypress (opens new window) 是 E2E 测试工具)
可见 Jest 是一个备受好评的测试工具
JS 生态中有很多类似的单元测试框架,
Jest 相比 Mocha 有更为清晰的说明文档,
并且和 AVA 一样支持并行测试提高效率。
配合衍生工具或其他类型的测试工具,
可以全面覆盖前端开发中的测试环节。
# 学习使用 Jest
# 概览
- 耗时:从入门到熟悉基本使用及 API,6~16 小时
- 难点:理解 Jest 的一些特性(快照、Timer 等)
- 工具:jest (opens new window)
# 学习路线
- 前置学习
- 学习 Jest
- 如何使用 Jest
- babel 支持及配置文件
- 练习文档中的所有
expect
API - Mock 方法
- 实战
- 为项目的业务逻辑代码编写单元测试
- 进阶
- Jest 概念
- 快照
- Timer
- Clear
- 生命周期钩子
- each
- Jest 概念
# 资料
# 自学教材
- Jest (opens new window):官方文档
- Jest - Learn By Doing (opens new window):我搭建的一个 Jest 基本使用环境
# Jest 知识体系
# Jest 运行配置
- 运行选项
jest.config.js
文件- 搭配
babel
支持ESNext
、TypeScript
等扩展语法
# Jest 基本 API
- 全局 API (opens new window)
- 基本测试 API
describe
、test
、expect
- 特化
.each(table)(name, fn, timeout)
.only(name, fn)
.skip(name, fn)
test.todo(name)
- 生命周期钩子
afterAll
、afterEach
、beforeAll
、beforeEach
- 基本测试 API
expect
API (opens new window)- 辅助
- 元检查器
expect.assertions(number)
:test
内必须有几次检查expect.hasAssertions()
:test
内必须有检查
- 静态对比对象
expect.anything()
:任意值expect.any(constructor)
:任意具体类型- 检查器的静态版
expect.arrayContaining(array)
:数组子集expect.objectContaining(object)
expect.stringContaining(string)
expect.stringMatching(string | regexp)
- 扩展
expect.extend(matchers)
:自定义编写的检查器
- 包装器
.not
:取反.resolves
、.rejects
:支持Promise
/async/await
- 元检查器
- 检查器
- 相等性
.toBe(value)
:检查规则为===
+Object.is
- 对象
- 类似
toBe
,支持对象的递归检查.toEqual(value)
:递归对比对象字段(鸭式辩型).toStrictEqual(value)
:toEqual
+ 对比显式和隐式undefined
.toMatchObject(object)
:toEqual
+ 支持不完整对比
- 其他
.toHaveProperty(keyPath, value?)
:对象字段检查.toBeInstanceOf(Class)
:instanceof
- 类似
- 数字
.toBeGreaterThan(number)
.toBeGreaterThanOrEqual(number)
.toBeLessThan(number)
.toBeLessThanOrEqual(number)
.toBeCloseTo(number, numDigits?)
:精度近似,解决 IEEE754 问题.toBeNaN()
:值是NaN
,Number.isNaN()
- 字符串/数组
.toMatch(regexpOrString)
:字符串的match
.toContain(item)
:字符串子串,数组项(严格对比).toContainEqual(item)
:数组子集(当数组项是对象时使用 Equal 对比).toHaveLength(number)
:.length
- 函数(mock 函数的调用检查)
.toHaveBeenCalled()
.toHaveBeenCalledTimes(number)
.toHaveBeenCalledWith(arg1, arg2, ...)
.toHaveBeenLastCalledWith(arg1, arg2, ...)
.toHaveBeenNthCalledWith(nthCall, arg1, arg2, ....)
.toHaveReturned()
.toHaveReturnedTimes(number)
.toHaveReturnedWith(value)
.toHaveLastReturnedWith(value)
.toHaveNthReturnedWith(nthCall, value)
.toThrow(error?)
:被检查函数应该报错
- Nullish
.toBeUndefined()
:值是undefined
(显式或隐式).toBeDefined()
:任意非undefined
值(包括 null).toBeNull()
:值是null
.toBeFalsy()
:假值,包括0
/""
/undefined
等隐式假值.toBeTruthy()
:真值,非假值的其他所有值
- 相等性
- 辅助
- Mock(模拟函数相关 API)
- 函数 mock 包装器 (opens new window)
jest.fn(implementation)
:mock 包装函数,以便支持toHaveBeenCalled
等检查器测试jest.spyOn(object, methodName, accessType?)
:相比jest.fn
,支持直接调用原始对象字段jest.isMockFunction(fn)
- mock 实例 (opens new window)
- 名字
mockFn.mockName(value)
mockFn.getMockName()
- 运行情况
mockFn.mock.calls
:传的参数mockFn.mock.results
:得到的返回值mockFn.mock.instances
:mock 包装器实例
- 模拟函数
mockFn.mockImplementation(fn)
:重新声明被 mock 的函数mockFn.mockImplementationOnce(fn)
- 模拟结果
mockFn.mockReturnThis()
mockFn.mockReturnValue(value)
mockFn.mockReturnValueOnce(value)
mockFn.mockResolvedValue(value)
mockFn.mockResolvedValueOnce(value)
mockFn.mockRejectedValue(value)
mockFn.mockRejectedValueOnce(value)
- 名字
- clear(TODO)
mockFn.mockClear()
mockFn.mockReset()
mockFn.mockRestore()
jest.clearAllMocks()
jest.resetAllMocks()
jest.restoreAllMocks()
- 函数 mock 包装器 (opens new window)
- 快照(TODO)
expect.addSnapshotSerializer(serializer)
.toMatchSnapshot(propertyMatchers?, hint?)
.toMatchInlineSnapshot(propertyMatchers?, inlineSnapshot)
.toThrowErrorMatchingSnapshot(hint?)
.toThrowErrorMatchingInlineSnapshot(inlineSnapshot)
- Timer(TODO)
# Jest 典型代码
参考 Jest - Learn By Doing (opens new window)
# 基本使用
// add.ts
export const add = (a: number, b: number) => a + b;
1
2
3
2
3
// add.test.ts
import { add } from './add';
test('test add function', () => {
expect(add(1, 2)).toBe(3);
});
1
2
3
4
5
6
7
2
3
4
5
6
7
describe('描述:基本使用', () => {
test('简单测试项', () => {
// expect.hasAssertions();
expect(2).toBe(2);
});
test('简单测试项2', () => {
// expect.assertions(2);
expect('hello').toEqual('hello');
expect('hello').not.toEqual('world');
});
});
describe('基本类型简单测试', () => {
test('数字', () => {
expect(0.1 + 0.2).not.toEqual(0.3);
expect(0.1 + 0.2).toBeCloseTo(0.3);
expect(1 + 2).toBeLessThanOrEqual(4);
expect(Infinity / Infinity).toBeNaN();
});
test('字符串/数组', () => {
expect('Quick fox').toMatch(/quick/i);
expect('Quick fox').toContain('ck fo');
expect(['Hello', 'world']).toContain('Hello');
expect([{ id: 1 }, { id: 2 }]).not.toContain({ id: 1 });
expect([{ id: 1 }, { id: 2 }]).toContainEqual({
id: 1,
});
expect([{ id: 1 }, { id: 2 }]).not.toContainEqual({
id: 1,
name: 'John',
});
expect('Quick').toHaveLength(5);
expect(['Hello', 'world']).toHaveLength(2);
});
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 假值检测
describe('Nullish', () => {
test('', () => {
const obj = {};
// @ts-ignore
expect(obj.key).toBeUndefined();
expect(undefined).toBeUndefined();
expect(undefined).not.toBeDefined();
expect(null).toBeDefined();
expect(0).toBeDefined();
expect(null).toBeNull();
expect(undefined).not.toBeNull();
expect(0).toBeFalsy();
expect('').toBeFalsy();
expect(null).toBeFalsy();
expect('0').toBeTruthy();
expect([]).toBeTruthy();
});
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 容易混淆的相等性 API
describe('容易混淆的相等性 API,对象', () => {
test('toBe', () => {
expect({ hello: 'world' }).not.toBe({ hello: 'world' });
const obj = { hello: 'world' };
expect(obj).toBe(obj);
});
test('toEqual', () => {
expect({ hello: 'world' }).toEqual({ hello: 'world' });
expect({ hello: 'world' }).toEqual({
hello: 'world',
un: undefined,
});
});
test('toStrictEqual', () => {
expect({ hello: 'world' }).toStrictEqual({
hello: 'world',
});
expect({ hello: 'world' }).not.toStrictEqual({
hello: 'world',
un: undefined,
});
});
test('toMatchObject', () => {
expect({
val: 2,
arr: [4, 3, 2, 1],
hello: 'world',
}).toMatchObject({
val: expect.any(Number),
arr: expect.arrayContaining([1, 2]),
});
});
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# Mock 系列
describe('Mock 系列', () => {
test('jest.fn', () => {
const hello = (name = 'John') => `Hello ${name}`;
const mockFn = jest.fn(hello);
mockFn.mockName('hello there');
mockFn('Chirs');
// console.log(mockFn.getMockName()); // => hello there
// console.log(mockFn.mock.calls); // => [ [ 'Chirs' ] ]
// console.log(mockFn.mock.results);
// => [ { type: 'return', value: 'Hello Chirs' } ]
// console.log(mockFn.mock.instances); // => [ undefined ]
expect(mockFn).toHaveBeenCalledTimes(1);
expect(mockFn).toHaveBeenCalledWith('Chirs');
expect(mockFn).toHaveReturnedTimes(1);
expect(mockFn).toHaveReturnedWith('Hello Chirs');
});
test('jest.spyOn', () => {
const obj = {
hello: (name = 'John') => `Hello ${name}`,
};
const spyFn = jest.spyOn(obj, 'hello');
obj.hello('Freddie');
obj.hello('Bob');
// console.log(spyFn.getMockName()); // => jest.fn()
// console.log(spyFn.mock.calls); // => [ [ 'Freddie' ], [ 'Bob' ] ]
// console.log(spyFn.mock.results); // =>
// [
// { type: 'return', value: 'Hello Freddie' },
// { type: 'return', value: 'Hello Bob' }
// ]
// console.log(spyFn.mock.instances);
// => [ { hello: [Function: mockConstructor] {} }, {...} ]
expect(spyFn).toHaveBeenCalled();
expect(spyFn).toHaveBeenCalledTimes(2);
expect(spyFn).toHaveBeenCalledWith('Freddie');
expect(spyFn).toHaveBeenLastCalledWith('Bob');
expect(spyFn).toHaveBeenNthCalledWith(1, 'Freddie');
expect(spyFn).toHaveReturned();
expect(spyFn).toHaveReturnedTimes(2);
expect(spyFn).toHaveReturnedWith('Hello Bob');
expect(spyFn).toHaveLastReturnedWith('Hello Bob');
expect(spyFn).toHaveNthReturnedWith(2, 'Hello Bob');
});
test('mock 实例方法', () => {
// * ---------------- 等价写法
{
const hello = (name = 'John') => `Hello ${name}`;
jest.fn(hello);
jest.fn().mockImplementation(hello);
}
// * ---------------- mockImplementationOnce
{
const myMockFn = jest
.fn()
.mockImplementationOnce((cb) => cb(null, true))
.mockImplementationOnce((cb) => cb(null, false));
const result = [
myMockFn((err, val) => val),
myMockFn((err, val) => val),
];
expect(result).toEqual([true, false]);
}
// * ---------------- mockReturnValueOnce
{
const myMockFn = jest
.fn()
.mockReturnValue('default')
.mockReturnValueOnce('first call')
.mockReturnValueOnce('second call');
expect([
myMockFn(),
myMockFn(),
myMockFn(),
myMockFn(),
]).toEqual([
'first call',
'second call',
'default',
'default',
]);
}
});
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
上次更新: Jan 29, 2022 6:01 PM