Jest

Jest 是一个 javacript 测试框架,特点是简单易上手

安装

1
npm install jest

需要 node 9.2 以上版本,因为 9.2 以下的版本不支持 try…catch 语法,运行时会报语法错误

Matcher

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 字面值使用 toBe
expect(2 + 2).toBe(4);

// 对象使用 toEqual
const data = {one: 1};
data['two'] = 2;
expect(data).toEqual({one: 1, two: 2});

// 取反使用 not
expect(4).not.toBe(3);

// 字符串可正则匹配
expect('team').not.toMatch(/I/);
expect('Christoph').toMatch(/stop/);

// 数组可判断是否包含
expect([1, 2]).toContain(1);

异步处理

回调模式

使用 done 来处理回调模式的异步函数

1
2
3
4
5
6
7
8
9
10
11
12
test('callback pattern', done => {
function callback(data) {
try {
expect(data).toBe('something');
done();
} catch (error) {
done(error);
}
}

fetchData(callback);
});

Promise 模式

返回 Promise 即可;

1
2
3
4
5
6
// 注意此处不要漏了写 return,不然实际返回的是 undefined;
test("promise pattern", () => {
return fetchData().then(data => {
expect(data).toBe('something');
})
})

Async/Await 模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
test('Async pattern', async () => {
const data = await fetchData();
expect(data).toBe('something');
});

// 带异常的情况
test('Async pattern with error', async () => {
try {
const data = await fetchData();
expect(data).toBe('something');
} catch (error) {
expect(error).toMatch('error');
}
})

初始化和清理

重复初始化

1
2
3
4
5
6
7
beforeEach(() => {
initDB(); // 如果 initDB 函数是异步的,则应为 return initDB();
});

afterEach(() => {
clearDB();
})

一次性初始化

1
2
3
4
5
6
7
beforeAll(() => {
return initDB();
})

afterAll(() => {
return clearDB();
})

作用域

使用 describe 来建立块的作用域

1
2
3
4
5
6
7
8
9
10
11
beforeEach('parent');
afterEach('parent');
test('parent1');
test('parent2');
describe('block', () => {
beforeEach('child');
afterEach('child');
test('child1');
test('child2');
});
// 注意:parent 的 beforeEach 会在 child 的 beforeEach 之前先执行;同时 parent 的 afterEach 在 child 的 afterEach 之后执行;

顺序控制

所有的 describe 将在所有的 test 之前先执行;

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
describe('outer', () => {
console.log('outer a');

describe('inner 1', () => {
console.log('inner 1');
test('test 1', () => {
console.log('test 1 inner 1');
});
});

console.log('outer b');

test('test 1', () => {
console.log('test 1 outer');
});

describe('inner 2', () => {
console.log('inner 2');
test('test 2', () => {
console.log('test 2 inner 2');
});
});

console.log('outer c');
});
// 实际的输出顺序如下:
// outer a
// inner 1
// outer b
// inner 2
// outer c
// test 1 inner 1
// test 1 outer
// test 2 inner 2

单例测试

test.only 方法可以使得只有该测试用例被执行;如果多例同时测试失败,但是单例测试可以成功,则说明某两个测试用例之间存在共享的变量,二者在测试过程中出现相互干扰的情况;

Mock 函数

mock 函数扮演拦截的作用,当在代码中对某个实际的函数进行调用时,拦截该调用,触发提前写好的 mock 函数,返回预设的结果;

用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const mockFunc = jest.fn(x => 42 + x);
forEach([0, 1], mockFunc);

// 检查 mockFunc 是否被调用了两次
expect(mockFunc.mock.calls.length).toBe(2);

// 检查第一次调用时传入的参数值是否为 0
expect(mockFunc.mock.calls[0][0]).toBe(0);

// 检查第二次调用时传入的参数值是否为 1
expect(mockFunc.mock.calls[1][0]).toBe(1);

// 检查第一次调用时的返回值是否为 42
expect(mockFunc.mock.results[0].value).toBe(42);

模拟返回值

1
2
3
4
5
6
7
const mock = jest.fn();
console.log(mock());
// > undefined

mock.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true);
console.log(mock(), mock(), mock(), mock());
// > 10, 'x', true, true

模拟函数在多次连续调用时,返回不同的值

1
2
3
4
5
6
7
8
9
10
11
12
const filterMock = jest.fn();

filterMock.mockReturnValueOnce(true).mockReturnValueOnce(false);

const result = [11, 12].filter(num => filterMock(num));

console.log(result);
console.log(filterMock.mock.calls[0][0]);
console.log(filterMock.mock.calls[1][0]);
// [11]
// 11
// 12

模拟模块的返回值

1
2
3
4
5
6
7
8
9
// users.js
// 此处假设 user.js 调用了 axios 模块,调用其中的 get 方法获取用户数据
import axios from 'axios';

Class Users {
static all() {
return axios.get('/user.json').then(resp => resp.data);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// user.test.js
import Users from './users.js';
import axios from 'axios';

jest.mock('axios');

test('should fetch users', () => {
const users = [{name: 'bob'}];
const resp = {data: users}; // 做了个假数据
axios.get.mockResolvedValue(resp); // 将假数据传进去

return Users.all().then(data => expect(data).toEqual(users));
})

模拟模块的实现

1
2
3
4
5
6
7
8
9
10
11
12
// foo.js
module.exports = function () {
// do something
}

// test.js
jest.mock('../foo'); // foo 模块已经被伪装
const foo = require('../foo'); // 此处调用的 foo 已经是一个模拟函数

foo.mockImplementation(() => 42); // 定义 foo 的行为
foo();
// > 42

模拟多次调用时,函数表现不同的行为

1
2
3
4
5
6
7
8
9
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

模块多次调用时,不同的行为 + 默认的行为

1
2
3
4
5
6
7
const myMockFn = jest
.fn(() => 'default')
.mockImplementationOnce(() => 'first call')
.mockImplementationOnce(() => 'second call');

console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn());
// > 'first call', 'second call', 'default', 'default'

为模拟函数添加名称

通过定义模拟的函数的名称,在测试的输出信息中,能够更清晰的定位哪个函数出错了

1
2
3
4
const mockFn = jest.fn()
.mockReturnValue('default')
.mockImplementation(scalar => scalar + 42)
.mockName('add42');

快照

用来比对界面是否与预期的一致;典型的使用方法是给某个 UI 组件预先存一份快照,然后与测试过程中生成的界面进行比对,检查二者是否一致,若一致,表示应用正常;如不一致,说明界面出现了意外的变化,此时有可能是应用出现异常,或者 快照本身需要更新;


Jest
https://ccw1078.github.io/2021/02/07/Jest/
作者
ccw
发布于
2021年2月7日
许可协议