# 4.开箱即用Jest
Jest是由Facebook发布的开源的、基于Jasmine (opens new window)的JavaScript单元测试框架。Jest源于Facebook的构想,用于快速、可靠地测试Web聊天应用。它吸引了公司内部的兴趣,Facebook的一名软件工程师Jeff Morrison半年前又重拾这个项目,改善它的性能,并将其开源。Jest的目标是减少开始测试一个项目所要花费的时间和认知负荷,因此它提供了大部分你需要的现成工具:快速的命令行接口、Mock工具集以及它的自动模块Mock系统。此外,如果你在寻找隔离工具例如Mock库,大部分其它工具将让你在测试中(甚至经常在你的主代码中)写一些不尽如人意的样板代码,以使其生效。Jest与Jasmine框架的区别是在后者之上增加了一些层。最值得注意的是,运行测试时,Jest会自动模拟依赖。Jest自动为每个依赖的模块生成Mock,并默认提供这些Mock,这样就可以很容易地隔离模块的依赖。
Jest支持Babel,我们将很轻松的使用ES6的高级语法
Jest支持webpack,非常方便的使用它来管理我们的项目
Jest支持TypeScript,书写测试用例更加严谨
简化API
Jest既简单又强大,内置支持以下功能:
- 灵活的配置:比如,可以用文件名通配符来检测测试文件。
- 测试的事前步骤(Setup)和事后步骤(Teardown),同时也包括测试范围。
- 匹配表达式(Matchers):能使用期望
expect
句法来验证不同的内容。 - 测试异步代码:支持承诺(promise)数据类型和异步等待
async
/await
功能。 - 模拟函数:可以修改或监查某个函数的行为。
- 手动模拟:测试代码时可以忽略模块的依存关系。
- 虚拟计时:帮助控制时间推移。
性能与隔离
Jest文档里写道:
Jest能运用所有的工作部分,并列运行测试,使性能最大化。终端上的信息经过缓冲,最后与测试结果一起打印出来。沙盒中生成的测试文件,以及自动全局状态在每个测试里都会得到重置,这样就不会出现两个测试冲突的情况。
Mocha用一个进程运行所有的测试,和它比较起来,Jest则完全不同。要在测试之间模拟出隔离效果,我们必须要引入几个测试辅助函数来妥善管理清除工作。这种做法虽然不怎么理想,但99%的情况都可以用,因为测试是按顺序进行的。
沉浸式监控模式
快速互动式监控模式可以监控到哪些测试文件有过改动,只运行与改动过的文件相关的测试,并且由于优化作用,能迅速放出监控信号。设置起来非常简单,而且还有一些别的选项,可以用文件名或测试名来过滤测试。我们用Mocha时也有监控模式,不过没有那么强大,要运行某个特定的测试文件夹或文件,就不得不自己创造解决方法,而这些功能Jest本身就已经提供了,不用花力气。
代码覆盖率&测试报告
Jest内置有代码覆盖率报告功能,设置起来易如反掌。可以在整个项目范围里收集代码覆盖率信息,包括未经受测试的文件。
要使完善Circle CI整合,只需要一个自定义报告功能。有了Jest,用jest-junit-reporter (opens new window)就可以做到,其用法和Mocha几乎相同。
快照功能
快照测试的目的不是要替换现有的单元测试,而是要使之更有价值,让测试更轻松。在某些情况下,某些功能比如React组件功能,有了快照测试意味着无需再做单元测试,但同样这两者不是非此即彼。
# 安装
新建文件夹然后通过npm 命令安装:
npm install --save-dev jest
或者通过yarn来安装:
yarn add --dev jest
然后就可以开始测试了
也可用npm install -g jest
进行全局安装;并在 package.json 中指定 test 脚本:
{
"scripts": {
"test": "jest"
}
}
2
3
4
5
Jest 的测试脚本名形如.test.js
,不论 Jest 是全局运行还是通过npm test
运行,它都会执行当前目录下所有的*.test.js
或 *.spec.js
文件、完成测试。
ES6语法支持:
- 安装依赖
yarn add --dev babel-jest @babel/core @babel/preset-env
- 配置
.babelrc
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
}
}
]
]
}
2
3
4
5
6
7
8
9
10
11
12
接下来就可以使用ES6的语法了~~~
更多高阶的ES6/7/8…语法,可以参见:babel官网 (opens new window)
关于Typescript的支持,可以参见 :Using Typescript (opens new window)
# 举个例子
关于test suite与test case:

describe属于 test suite 的描述,而每个 test 或者 it 则描述了每个 test case。
例如:math.js
export const add = (a, b) => a + b;
export const multiple = (a, b) => a * b;
2
3
测试脚本:math.test.js
const math = require('./src/math')
describe("math", () => {
let a
let b
beforeEach(function () {
a = 2;
b = 3;
});
test("#should return result as a+b", () => {
// test code
const result = math.add(a, b)
expect(result).toEqual(5)
});
it("#should return result as a*b", () => {
//test code
const result = math.multiple(a, b)
expect(result).toEqual(6)
});
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
test suite 可以进行嵌套:
describe("foo", () => {
describe("bar", () => {
it("foo bar", () => {
//test code
});
});
});
2
3
4
5
6
7
test case 也可以脱离 test suite 独立运行:
// hello.js
module.exports = () => 'Hello world'
// hello.test.js
let hello = require('hello.js')
test('should get "Hello world"', () => {
expect(hello()).toBe('Hello world') // 测试成功
// expect(hello()).toBe('Hello') // 测试失败
})
2
3
4
5
6
7
8
9
10
# Mock与Spy
mock测试就是在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便测试的测试方法。
Mock 是单元测试中经常使用的一种技术。单元测试,顾名思义测试的重点是某个具体单元。但是在实际代码中,代码与代码之间,模块与模块之间总是会存在着相互引用。这个时候,剥离出这种单元的依赖,让测试更加独立,使用到的技术就是 Mock。
为什么要使用Mock函数?
在项目中,一个模块的方法内常常会去调用另外一个模块的方法。在单元测试中,我们可能并不需要关心内部调用的方法的执行过程和结果,只想知道它是否被正确调用即可,甚至会指定该函数的返回值。此时,使用Mock函数是十分有必要。
Mock函数提供的以下三种特性,在我们写测试代码时十分有用:
- 捕获函数调用情况
- 设置函数返回值
- 改变函数的内部实现

举个例子:
// math.js
export const getFooResult = () => {
// foo logic here
};
export const getBarResult = () => {
// bar logic here
};
// caculate.js
import { getFooResult, getBarResult } from "./math";
export const getFooBarResult = () => getFooResult() + getBarResult();
2
3
4
5
6
7
8
9
10
11
12
此时,getFooResult() 和 getBarResult() 就是 getFooBarResult 这个函数的依赖。如果我们关注的点是 getFooBarResult 这个函数,我们就应该把 getFooResult 和 getBarResult Mock 掉,剥离这种依赖。下面是一个使用 Jest 进行 Mock 的例子。
jest.fn()
是创建Mock函数最简单的方式,如果没有定义函数内部的实现,jest.fn()
会返回undefined
作为返回值。
test('测试jest.fn()调用', () => {
let mockFn = jest.fn();
let result = mockFn(1, 2, 3);
// 断言mockFn的执行后返回undefined
expect(result).toBeUndefined();
// 断言mockFn被调用
expect(mockFn).toBeCalled();
// 断言mockFn被调用了一次
expect(mockFn).toBeCalledTimes(1);
// 断言mockFn传入的参数为1, 2, 3
expect(mockFn).toHaveBeenCalledWith(1, 2, 3);
})
2
3
4
5
6
7
8
9
10
11
12
13
情景一:设置函数 的返回值
// calculate.test.js
import { getFooBarResult } from "./calculate";
import * as fooBar from './math';
test('getResult should return result getFooResult() + getBarResult()', () => {
// mock add方法和multiple方法
fooBar.getFooBarResult = jest.fn(() => 10);
fooBar.getBarResult = jest.fn(() => 5);
const result = getFooBarResult();
expect(result).toEqual(15);
});
2
3
4
5
6
7
8
9
10
11
12
13
Mock其实就是一种Spies,在Jest中使用spies来“spy”(窥探)一个函数的行为。
Jest文档 (opens new window)对于spies的解释:
Mock函数也称为“spies”,因为它们让你窥探一些由其他代码间接调用的函数的行为,而不仅仅是测试输出。你可以通过使用
jest.fn()
创建一个mock函数。
简单来说,一个spy是另一个内置的能够记录对其调用细节的函数:调用它的次数,使用什么参数。
// calculate.test.js
import { getFooBarResult } from "./calculate";
import * as fooBar from './math';
test('getResult should return result getFooResult() + getBarResult()', () => {
// mock add方法和multiple方法
fooBar.getFooResult = jest.fn(() => 10);
fooBar.getBarResult = jest.fn(() => 5);
const result = getFooBarResult();
// 监控getFooResult和getBarResult的调用情况.
expect(fooBar.getFooResult).toHaveBeenCalled();
expect(fooBar.getBarResult).toHaveBeenCalled();
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
情景二:捕获函数调用情况
// bot method
const bot = {
sayHello: name => {
console.log(`Hello ${name}!`);
}
};
// test.js
describe("bot", () => {
it("should say hello", () => {
const spy = jest.spyOn(bot, "sayHello");
bot.sayHello("Michael");
expect(spy).toHaveBeenCalledWith("Michael");
spy.mockRestore();
});
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
我们通过 jest.spyOn
创建了一个监听 bot
对象的 sayHello
方法的 spy。它就像间谍一样监听了所有对 bot#sayHello
方法的调用。由于创建 spy 时,Jest 实际上修改了 bot
对象的 sayHello
属性,所以在断言完成后,我们还要通过 mockRestore
来恢复 bot
对象原本的 sayHello
方法。
Jest的spyOn介绍 (opens new window)
情景三:修改函数的内容实现
const bot = {
sayHello: name => {
console.log(`Hello ${name}!`);
}
};
describe("bot", () => {
it("should say hello", () => {
const spy = jest.spyOn(bot, "sayHello").mockImplementation(name => {
console.log(`Hello mix ${name}`)
});
bot.sayHello("Michael");
expect(spy).toHaveBeenCalledWith("Michael");
spy.mockRestore();
});
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
使用spyOn方法,还可以去修改Math.random这样的函数
jest.spyOn(Math, "random").mockImplementation(() => 0.9);
举个例子:
// getNum.js
const arr = [1,2,3,4,5,6];
const getNum = index => {
if (index) {
return arr[index % 6];
} else {
return arr[Math.floor(Math.random() * 6)];
}
};
// num.test.js
import { getNum } from '../src/getNum'
describe("getNum", () => {
it("should select numbber based on index if provided", () => {
expect(getNum(1)).toBe(2);
});
it("should select a random number based on Math.random if skuId not available", () => {
const spy = jest.spyOn(Math, "random").mockImplementation(() => 0.9);
expect(getNum()).toBe(6);
expect(spy).toHaveBeenCalled();
spy.mockRestore();
});
});
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
# CLI命令
➜ npx jest --help
Usage: jest [--config=<pathToConfigFile>] [TestPathPattern]
选项:
--help, -h 显示帮助信息 [布尔]
--version, -v Print the version and exit [布尔]
--config, -c The path to a jest config file specifying how to
find and execute tests. If no rootDir is set in
the config, the directory containing the config
file is assumed to be the rootDir for the
project.This can also be a JSON encoded value
which Jest will use as configuration. [字符串]
--coverage Indicates that test coverage information should
be collected and reported in the output. [布尔]
--timers Setting this value to fake allows the use of
fake timers for functions such as setTimeout.
[字符串]
--verbose Display individual test results with the test
suite hierarchy. [布尔]
--watch Watch files for changes and rerun tests related
to changed files. If you want to re-run all
tests when a file has changed, use the
`--watchAll` option. [布尔]
--watchAll Watch files for changes and rerun all tests. If
you want to re-run only the tests related to the
changed files, use the `--watch` option. [布尔]
...
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
常见使用:
--verbose
显示详细的测试信息,包括测试suite和case:
➜ npx jest --verbose
PASS test/mock.test.js
bot
✓ should say hello (7ms)
console.log test/mock.test.js:10
Hello mix Michael
PASS test/domain.test.js
getImageDomain
✓ should select domain based on skuId if provided (1ms)
✓ should select a random domain based on Math.random if skuId not available (1ms)
console.log test/sayhello.test.js:3
Hello Michael!
PASS test/sayhello.test.js
bot
✓ should say hello (6ms)
PASS test/num.test.js
getNum
✓ should select numbber based on index if provided (1ms)
✓ should select a random number based on Math.random if skuId not available
PASS test/math.test.js
math
✓ #should return result as a+b (1ms)
✓ #should return result as a*b (4ms)
Test Suites: 5 passed, 5 total
Tests: 8 passed, 8 total
Snapshots: 0 total
Time: 1.075s
Ran all test suites.
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
--watch
和--watchAll
用来监听测试文件的变化
Ran all test suites.
Watch Usage
› Press f to run only failed tests.
› Press o to only run tests related to changed files.
› Press p to filter by a filename regex pattern.
› Press t to filter by a test name regex pattern.
› Press q to quit watch mode.
› Press Enter to trigger a test run.
2
3
4
5
6
7
8
9
--coverage
用来形成测试覆盖率报告
-----------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
-----------|----------|----------|----------|----------|-------------------|
All files | 100 | 100 | 100 | 100 | |
getNum.js | 100 | 100 | 100 | 100 | |
math.js | 100 | 100 | 100 | 100 | |
-----------|----------|----------|----------|----------|-------------------|
Test Suites: 5 passed, 5 total
Tests: 8 passed, 8 total
Snapshots: 0 total
Time: 1.497s
2
3
4
5
6
7
8
9
10
11
12
# Jest在React、Vue项目中的应用
在create-react-app
中的应用:
安装对应的依赖:
npm install react-test-renderer enzyme enzyme-adapter-react-16
Enzyme (opens new window)是一个非常棒的React组件测试测试库。
Enzyme is a JavaScript Testing utility for React that makes it easier to test your React Components' output. You can also manipulate, traverse, and in some ways simulate runtime given the output.
Enzyme's API is meant to be intuitive and flexible by mimicking jQuery's API for DOM manipulation and traversal
需要注意的两点是:
- 需要配置Adapter,不同的React的Adapter不同
Enzyme Adapter Package | React semver compatibility |
---|---|
enzyme-adapter-react-16 | ^16.4.0-0 |
enzyme-adapter-react-16.3 | ~16.3.0-0 |
enzyme-adapter-react-16.2 | ~16.2 |
enzyme-adapter-react-16.1 | ~16.0.0-0 || ~16.1 |
enzyme-adapter-react-15 | ^15.5.0 |
enzyme-adapter-react-15.4 | 15.0.0-0 - 15.4.x |
enzyme-adapter-react-14 | ^0.14.0 |
enzyme-adapter-react-13 | ^0.13.0 |
需要初始化配置
setUpTests.js
官方文档在Package.json中设置jest配置,已经过时,Jest框架最新会默认加载文件
src/setUpTests.js
import { configure } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16' configure({ adapter: new Adapter() })
1
2
3
4
在vue工程化项目中:
"devDependencies": {
"@vue/cli-plugin-unit-jest": "^3.9.0",
"@vue/test-utils": "1.0.0-beta.29",
"babel-core": "7.0.0-bridge.0",
"babel-eslint": "^10.0.1",
"babel-jest": "^23.6.0",
"babel-preset-env": "^1.7.0",
},
2
3
4
5
6
7
8
添加如上依赖,
配置scripts:
"scripts": {
"test": "vue-cli-service test:unit"
},
2
3
配置jest.config.js
module.exports = {
// 处理vue结尾的文件
moduleFileExtensions: [
'js',
'jsx',
'json',
'vue'
],
// es6转义
transform: {
'^.+\\.vue$': 'vue-jest',
'.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
'^.+\\.jsx?$': 'babel-jest'
},
transformIgnorePatterns: [
'/node_modules/'
],
// cli配置了webpack别名
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
},
snapshotSerializers: [
'jest-serializer-vue'
],
testMatch: [
'**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
],
testURL: 'http://localhost/',
watchPlugins: [
'jest-watch-typeahead/filename',
'jest-watch-typeahead/testname'
]
}
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