# Jest
Jest 是一款优雅、简洁的 JavaScript 测试框架。同时 Jest 支持 Babel (opens new window)、TypeScript (opens new window)、Node (opens new window)、React (opens new window)、Angular (opens new window)、Vue (opens new window) 等诸多框架。
# 安装
$ npm install jest -D
添加 test
script
// package.json
{
"scripts": {
"test": "jest"
}
}
2
3
4
5
6
使用 babel
$ npm install babel-jest @babel/core @babel/preset-env -D
新建 babel.config.js
文件
module.exports = {
presets: [['@babel/preset-env', {targets: {node: 'current'}}]],
};
2
3
# Jest 的基本流程
Jest 使用匹配器进行测试,例如 toBe()
.
test("adds 1 + 2 to equal 3", () => {
expect(sum(1, 2)).toBe(3);
});
2
3
Jest 支持很多匹配器,详情请参考 Expect (opens new window).
Jest 还可以使用 describe()
将多个相关的测试组合在一起,例如:
describe('math', () => {
test('add', () => {
expect(1 + 2).toBe(3);
});
test('subtract', () => {
expect(3 - 1).toBe(2);
});
});
2
3
4
5
6
7
8
9
# 自定义匹配器
可以使用 expect.extend(matchers)
(opens new window) 创建自定义匹配器。
匹配器返回一个对象:{ pass: boolean, message: () => string }
, pass
确定是否匹配,message
返回错误信息。
当 pass
为 true
时,message
应该返回 expect(x).not.yourMatcher()
失败时的错误信息;
当 pass
为 false
时,message
应该返回 expect(x).yourMatcher()
失败时的错误信息。
例如,创建一个匹配范围的自定义匹配器 toBeWithinRange
import {expect} from '@jest/globals';
function toBeWithinRange(actual, floor, ceiling) {
if (
typeof actual !== 'number' ||
typeof floor !== 'number' ||
typeof ceiling !== 'number'
) {
throw new Error('These must be of type number!');
}
const pass = actual >= floor && actual <= ceiling;
const message = () => {
return pass
? `expected ${this.utils.printReceived(
actual
)} not to be within range ${this.utils.printExpected(
`${floor} - ${ceiling}`
)}`
: `expected ${this.utils.printReceived(
actual
)} to be within range ${this.utils.printExpected(
`${floor} - ${ceiling}`
)}`;
};
return { pass, message };
}
expect.extend({
toBeWithinRange,
});
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
import '../toBeWithinRange';
test('is within range', () => expect(100).toBeWithinRange(90, 110));
test('is NOT within range', () => expect(101).not.toBeWithinRange(0, 100));
test('asymmetric ranges', () => {
expect({apples: 6, bananas: 3}).toEqual({
apples: expect.toBeWithinRange(1, 10),
bananas: expect.not.toBeWithinRange(11, 20),
});
});
2
3
4
5
6
7
8
9
10
如果不想每个 test 文件都引入 toBeWithinRange
,可以将 expect.extend
移入 setupFilesAfterEnv
(opens new window) 脚本中。
自定义匹配器也支持异步,返回 { pass: boolean, message: string }
promise。
# 测试异步代码
Jest 也能测试异步代码,有三种方法:
# 返回 Promise
通过在测试里返回 promise,Jest 会等待 promise 完成,然后进行相应的测试。
test("test promise that resolved by returning promise", () => {
return fetchData().then(data => {
expect(data).toBe("data");
});
});
test("test promise that rejected by returning promise", () => {
expect.assertions(1);
return fetchDataWithError().catch(error => {
expect(error).toBe("error");
});
});
2
3
4
5
6
7
8
9
10
11
12
当测试 promise 应该 rejected
时,需要设置 expect.assertions(1)
,否则当 promise resolved
的时候,测试是通过的。
# Async/Await
Jest 也可以使用 async/await
测试异步代码。
test("test promise that resolved with async/await", async () => {
const data = await fetchData();
expect(data).toBe("data");
});
test("test promise that rejected by async/await", async () => {
expect.assertions(1);
try {
await fetchDataWithError();
} catch (error) {
expect(error).toBe("error");
}
});
2
3
4
5
6
7
8
9
10
11
12
13
同样当测试 promise 应该 rejected
时,需要设置 expect.assertions(1)
.
# resolves
/ rejects
修饰符
结合 async/await
和 .resolves
/ .rejects
修饰符,可以很方便的测试异步代码。
test("test promise that resolved by `resolves` modifier", async () => {
await expect(fetchData()).resolves.toBe("data");
});
test("test promise that rejected with `rejects` modifier", async () => {
await expect(fetchDataWithError()).rejects.toBe("error");
});
2
3
4
5
6
7
# 回调
Jest 通过提供 done
参数来测试 callback.
test("callback", done => {
function callback(error, data) {
if (error) {
done(error);
return;
}
try {
expect(data).toBe("data");
done();
} catch (error) {
done(error);
}
}
fetchDataWithCallback(callback);
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Jest 会等 done
回调函数被调用执行结束后,再结束测试。
# 安装和移除
写测试的时候需要在测试前做一些准备工作,Jest 提供了 beforeEach
和 beforeAll
辅助函数,
相应地需要在测试后进行一些收尾工作,Jest 提供了 afterEach
和 afterAll
辅助函数。
执行顺序如下:
beforeAll(() => console.log('1 - beforeAll'));
afterAll(() => console.log('1 - afterAll'));
beforeEach(() => console.log('1 - beforeEach'));
afterEach(() => console.log('1 - afterEach'));
test('', () => console.log('1 - test'));
describe('Scoped / Nested block', () => {
beforeAll(() => console.log('2 - beforeAll'));
afterAll(() => console.log('2 - afterAll'));
beforeEach(() => console.log('2 - beforeEach'));
afterEach(() => console.log('2 - afterEach'));
test('', () => console.log('2 - test'));
});
// 1 - beforeAll
// 1 - beforeEach
// 1 - test
// 1 - afterEach
// 2 - beforeAll
// 1 - beforeEach
// 2 - beforeEach
// 2 - test
// 2 - afterEach
// 1 - afterEach
// 2 - afterAll
// 1 - afterAll
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
当一个文件有多个 beforeEach
,以它们定义的顺序执行;
当一个文件有多个 afterEach
时,以它们定义的顺序反向执行。
# 模拟函数
模拟函数允许你通过擦除函数的实际实现来测试代码之间的链接。
# 使用 Mock 函数
Jest 通过 jest.fn
定义一个 mock 函数,来代替真正的函数,然后可以对这个 mock 函数进行测试,比如测试被调用的次数、调用时的传入的参数、函数的返回值等等。
const mockCallback = jest.fn(x => 42 + x);
test('forEach mock function', () => {
forEach([0, 1], mockCallback);
// The mock function was called twice
expect(mockCallback.mock.calls).toHaveLength(2);
// The first argument of the first call to the function was 0
expect(mockCallback.mock.calls[0][0]).toBe(0);
// The first argument of the second call to the function was 1
expect(mockCallback.mock.calls[1][0]).toBe(1);
// The return value of the first call to the function was 42
expect(mockCallback.mock.results[0].value).toBe(42);
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mock 函数有一个 mock
属性,定义了很多与函数调用相关的属性,详情请参考 Mock Functions (opens new window).
# 模拟实现
通过 jest.mockImplementation
和 jest.mockImplementationOnce
方法来替换函数的实现。
jest.fn(x => 42 + x)
等价于 jest.fn().mockImplementation(x => 42 + x)
。
const foo = jest
.fn(() => "never call")
.mockImplementationOnce(() => "first call")
.mockImplementationOnce(() => "second call")
.mockImplementation(() => "default");
test("mock implementation", () => {
expect(foo()).toBe("first call");
expect(foo()).toBe("second call");
expect(foo()).toBe("default");
expect(foo()).toBe("default");
});
2
3
4
5
6
7
8
9
10
11
# 模拟返回值
如果 mock 函数只是返回一个值,可以使用 jest.mockReturnValue
和 jest.mockReturnValueOnce
代替
const fn = jest.mockReturnValue(10)
// 等价于
// const fn = jest.fn(() => 10);
// const fn = jest.mockImplementation(() => 10);
2
3
4
# 模拟返回 this
有时候为了方便级联调用,需要返回 this
,可以使用 jest.mockReturnThis
const myObj = {
myMethod: jest.fn().mockReturnThis(),
};
/*
等价于
const myObj = {
myMethod: jest.fn(function () {
return this;
}),
};
*/
2
3
4
5
6
7
8
9
10
11
12
# 模拟模块
# 模拟整个模块
Jest 可以 mock 整个模块,例如下面的代码 mock axios
模块
import axios from 'axios';
import Users from './users';
jest.mock('axios');
test('should fetch users', () => {
const users = [{name: 'Bob'}];
const resp = {data: users};
// 或者使用 `mockImplementation`
// axios.get.mockImplementation(() => Promise.resolve(resp))
axios.get.mockResolvedValue(resp);
return Users.all().then(data => expect(data).toEqual(users));
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 模拟部分模块
Jest 也可以 mock 模块的一部分,例如
// foo-bar-baz.js
export const foo = "foo";
export const bar = () => "bar";
export default () => "baz";
2
3
4
然后我们可以使用 jest.mock(moduleName, factory)
模拟 foo-bar-baz
的部分实现,
关键点是使用 jest.requireActual
获取模块原来的定义,然后覆盖部分实现。
import defaultExport, {bar, foo} from './foo-bar-baz';
jest.mock('../foo-bar-baz', () => {
const originalModule = jest.requireActual('../foo-bar-baz');
return {
__esModule: true, // Use it when dealing with esModules
...originalModule,
default: jest.fn(() => 'mocked baz'),
foo: 'mocked foo',
};
});
test('should do a partial mock', () => {
const defaultExportResult = defaultExport();
expect(defaultExportResult).toBe('mocked baz');
expect(defaultExport).toHaveBeenCalled();
expect(foo).toBe('mocked foo');
expect(bar()).toBe('bar');
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 手动模拟
# 模拟用户模块
手动模拟自定义模块,需要在模块同级目录下创建一个 __mocks__
目录,然后在这个目录下新建模块同名文件。比如要手动模拟 modules/user.js
,需要创建 modules/__mocks__/user.js
。
// modules/__mocks__/user.js
const user = jest.createMockFromModule("../user.js");
user.upperName = jest.fn(() => "MOCK")
export default user;
2
3
4
然后在测试的时候,调用 jest.mock('./moduleName')
,调用模块的 mock 实现。
import user from "../src/modules/user"
jest.mock('../src/modules/user');
test("manual mock", () => {
expect(user.upperName("anything")).toBe("MOCK");
});
2
3
4
5
6
当
automock
打开时,即使没有调用jest.mock('./moduleName')
,在测试时使用的也是模块的 mock 实现。如果不想要 mock 实现,则需要明确调用jest.unmock('./moduleName')
。
# 模拟 Node 模块
Jest 除了可以模拟自定义模块,还可以模拟第三方 node modules。在 node_modules
同级目录下创建 __mocks__
目录,然后在这个目录下新建模块同名文件,比如要手动模拟 axios
,创建 projectName/__mocks__/axios.js
。
如果是 scoped modules,例如:模拟 @scope/project-name
模块,则创建 __mocks__/@scope/project-name.js
文件。
在测试的时候,不需要像模拟自定义模块一样,调用 jest.mock('axios')
。但是如果是模拟 Node 的内置模块(例如:fs
或 path
),则需要显式调用 jest.mock("path")
,因为默认情况下内置模块不会被模拟。
# 模拟模块的一个方法
如果只想模拟模块的一个方法,有下面三种方式:
# 手动模拟
引入 Module,然后 mock 掉想要 mock 的方法
import * as moduleApi from '@module/api';
moduleApi.functionToMock = jest.fn().mockReturnValue({ someObjectProperty: 42 });
2
3
# jest.spyOn(object, methodName)
jest.spyOn
(opens new window) 创建一个类似于 jest.fn
的模拟函数,但也跟踪调用 object[methodName]
import * as moduleApi from '@module/api';
jest.spyOn(moduleApi, 'functionToMock').mockReturnValue({ someObjectProperty: 42 });
2
3
# jest.requireActual(moduleName)
通过在 jest.mock(path, moduleFactory)
里调用 jest.requireActual(moduleName)
,取得模块的真实实现,然后 mock 掉想要 mock 的方法。
import { functionToMock } from "@module/api";
jest.mock("@module/api", () => {
const original = jest.requireActual("@module/api");
return {
...original,
functionToMock: jest.fn()
};
});
2
3
4
5
6
7
8
9
# 模拟 ES6 类
ES6 类其实就是一个带有语法糖的构造函数,因此可以使用 mock 函数来模拟 ES6 类。有四种方式:
// sound-player.js
export default class SoundPlayer {
constructor() {
this.foo = 'bar';
}
playSoundFile(fileName) {
console.log('Playing sound file ' + fileName);
}
}
// sound-player-consumer.js
import SoundPlayer from './sound-player';
export default class SoundPlayerConsumer {
constructor() {
this.soundPlayer = new SoundPlayer();
}
playSomethingCool() {
const coolSoundFileName = 'song.mp3';
this.soundPlayer.playSoundFile(coolSoundFileName);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 自动模拟
调用 jest.mock('./sound-player')
,将返回一个自动模拟(automatic mock)的版本 ,它将 ES6 类替换为模拟构造函数,并将其所有方法替换为总是返回 undefined
的模拟函数。
# 手动模拟
在 __mocks__
目录下创建手动 mock
// __mocks__/sound-player.js
export const mockPlaySoundFile = jest.fn();
const mock = jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
export default mock;
2
3
4
5
6
7
或者另一个 class
// __mocks__/sound-player.js
export default class SoundPlayer {
constructor() {
console.log('Mock SoundPlayer: constructor was called');
}
playSoundFile() {
console.log('Mock SoundPlayer: playSoundFile was called');
}
}
2
3
4
5
6
7
8
9
10
# jest.mock(path, moduleFactory)
jest.mock(path, moduleFactory)
接受一个模块工厂参数。模块工厂是一个返回模拟的函数。
import SoundPlayer from './sound-player';
const mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
return jest.fn().mockImplementation(() => {
return {playSoundFile: mockPlaySoundFile};
});
});
2
3
4
5
6
7
由于对
Jest .mock()
的调用被提升到文件的顶部,因此 Jest 阻止对范围外变量的访问。默认情况下,不能先定义变量,然后在工厂中使用它。但是 Jest 将对以 mock 开头的变量禁用此类检查。然而,它仍然是由您来保证他们将被初始化的时间。
# mockImplementation()
您可以通过在现有模拟上调用 mockImplementation()
来替换上述所有模拟,以便更改单个测试或所有测试的实现。
import SoundPlayer from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';
jest.mock('./sound-player');
describe('When SoundPlayer throws an error', () => {
beforeAll(() => {
SoundPlayer.mockImplementation(() => {
return {
playSoundFile: () => {
throw new Error('Test error');
},
};
});
});
it('Should throw an error when calling playSomethingCool', () => {
const soundPlayerConsumer = new SoundPlayerConsumer();
expect(() => soundPlayerConsumer.playSomethingCool()).toThrow();
});
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 模拟类的某个方法
可以使用 spyOn
mock 类的某个方法
import SoundPlayer from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';
const playSoundFileMock = jest
.spyOn(SoundPlayer.prototype, 'playSoundFile')
.mockImplementation(() => {
console.log('mocked function');
});
it('player consumer plays music', () => {
const player = new SoundPlayerConsumer();
player.playSomethingCool();
expect(playSoundFileMock).toHaveBeenCalled();
});
2
3
4
5
6
7
8
9
10
11
12
13
14
类的静态方法、getter/setter
方法也可以使用这种方式
import SoundPlayer from './sound-player';
const staticMethodMock = jest
.spyOn(SoundPlayer, 'brand') // 静态方法
.mockImplementation(() => 'some-mocked-brand');
const getterMethodMock = jest
.spyOn(SoundPlayer.prototype, 'foo', 'get') // getter 方法
.mockImplementation(() => 'some-mocked-result');
it('custom methods are called', () => {
const player = new SoundPlayer();
const foo = player.foo;
const brand = SoundPlayer.brand();
expect(staticMethodMock).toHaveBeenCalled();
expect(getterMethodMock).toHaveBeenCalled();
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Demo
# References
- Jest (opens new window)
- How To Mock Only One Function From A Module In Jest (opens new window)
awesome-jest
(opens new window)vscode-jest
(opens new window)jest-extended
(opens new window)eslint-plugin-jest
(opens new window)jest-matcher-utils
(opens new window)@sinonjs/fake-timers
(opens new window)pretty-format
(opens new window)snapshot-diff
(opens new window)