开始
Jest是由Facebook发布的开源的、基于Jasmine的Javascript的单元测试框架。官方文档在这里。
安装:
npm install --save-dev jest
然后创建一个sum.js
文件,输出一个方法:
function sum(a, b) {
return a + b;
}
module.exports = sum;
然后,创建一个名为sum.test.js
的文件。这将包含我们的实际测试︰
const sum = require('./sum');
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
然后将下面的配置部分添加到你的package.json
里面:
{
"scripts": {
"test": "jest"
}
}
然后就可以通过npm run test
来启动单元测试,测试结果:
PASS ./sum.test.js
✓ adds 1 + 2 to equal 3 (5ms)
webstorm增加Jest语法提示
WebStorm
→ Preferences
→ Languages & Frameworks
→ JavaScript
→ Libraries
, 点击Download
然后找到列表页中的Jest
, 然后点击Download and Install
.
匹配器
Jest通过匹配器来测试方法的结果是否符合预期,完整的API列表在这里。
普通匹配器
最简单的测试值的方法是看是否精确匹配
test('two plus two is four', () => {
expect(2 + 2).toBe(4);
});
expect
的用处调用被测试的函数或者方法,返回一个结果toBe
就是匹配器,判断结果和期望是否相等
toBe
使用的是Object.is()
判断相等,如果要检查对象的值,需要使用toEqual
test('object', () => {
let obj = {value: 'a'};
expect(obj.value).toEqual('a')
});
真假值
JS中,undefined
、null
、false
有时需要区分,有时不需要,所以Jest提供了下面的API来进行匹配:
- toBeNull
:只匹配null
- toBeUndefined
:只匹配undefined
- toBeDefined
:与toBeUndefined
相反
- toBeNull
:只匹配null
- toBeTruthy
:匹配任何if
语句为真
- toBeFalsy
:匹配任何if
语句为假
数字
对于数字有各种等价的匹配器
- toBe
:=
- toEqual
:=
- toBeGreaterThan
:>
- toBeGreaterThanOrEqual
:≥
- toBeLessThan
:<
- toBeLessThanOrEqual
:≤
对于浮点数,应该使用toBeClose
而不是toEqual
test('两个浮点数字相加', () => {
const value = 0.1 + 0.2;
//expect(value).toBe(0.3); 这句会报错,因为浮点数有舍入误差
expect(value).toBeCloseTo(0.3); // 这句可以运行
});
字符串
使用toMatch
来验证字符串中是否包含指定的字符串或者正则表达式
test('this is a joe in the sentence?', () => {
expect('this is joe').toMatch(/joe/)
});
数组
使用toContain
来验证数组中是否包含匹配子项
test('the Array contains the item?', () => {
const arr = ['a', 'n', 'cc'];
expect(arr).toContain('cc')
});
异常
如果一个函数在某些情况下会抛出错误,Jest也可以对这种情况进行预期,使用的API是toThrow
function fn() {
throw new Error('this is a error')
}
test('function will throw an error', () => {
expect(fn).toThrow();
expect(fn).toThrow(Error);
expect(fn).toThrow('this is a error');
expect(fn).toThrow(/is/);
});
完整的API列表在这里。
测试异步代码
异步代码的测试是很常见的一个需求,当运行异步代码时,Jest需要知道它当前测试的异步代码什么时候完成,才可以运行下一个测试。Jest 有多种办法来处理这种情况。
回调
最常见的异步模式是回调函数。
举个例子,假设你有一个叫做fetchData(callback)
的函数,可以去获取一些数据,拿到数据之后就会调用callback(data)
。你想要这个返回的数据是一个'peanut butter'
字符串。默认情况下,Jest测试走到它们运行期的结束阶段就算是结束了。这意味着这个测试不会按照你预期的那样运行:
// Don't do this!
test('the data is peanut butter', () => {
function callback(data) {
expect(data).toBe('peanut butter');
}
fetchData(callback);
});
问题在于这个测试在fetchData
运行完的那一瞬间就算结束了,它根本就来不及去调用那个回调函数(callback
)。
为了解决这个问题,引入done()
单参数,Jest会等待done
结束执行后结束执行
test('the data is peanut butter', done => {
function callback(data) {
expect(data).toBe('peanut butter');
done();
}
fetchData(callback);
});
如果done()
没有被调用,测试就会失败,这也是你所希望发生的。
Promise
如果异步代码使用了Promise,处理方法更加简单,只需要在测试中返回一个Promise,Jest会等待Promise完成在进行校验,如果Promise被异常reject
,则测试失败
function fetchDate(callBack) {
return new Promise((resolve, reject) => {
setTimeout(resolve, 3000, 'ok')
})
}
test('the data is ok', () => {
expect.assertions(1);
return fetchDate().then(val => {
expect(val).toBe('ok')
})
});
上面的
expect.assertions(1)
的意思是指定这个测试中的断言的数量,在测试异步代码时经常会用到这个方法,确保异步代码的全部的回调函数都被调用
注意:测试方法一定要return
一个promise
,否则测试会在异步调用发起之后立刻结束
如果Promise的结果是reject
,需要使用.catch
方法
function fetchDate(callBack) {
return new Promise((resolve, reject) => {
setTimeout(reject, 3000, 'ok')
})
}
test('the data is ok', () => {
expect.assertions(1);
return fetchDate().catch(val => {
expect(val).toBe('ok')
})
});
then()
方法可以用resolves
方法替代,catch
方法可以用rejects
方法替代:
test('the data is ok', () => {
expect.assertions(1);
return expect(fetchDate()).resolves.toBe('ok')
});
test('the data is ok', () => {
expect.assertions(1);
return expect(fetchDate()).rejects.toBe('ok')
});
Async/Await
可以在测试中使用async
和await
。 若要编写async
测试,只要在传给test
的函数前面加上async
关键字就行了。
test('the data is peanut butter', async () => {
expect.assertions(1);
const data = await fetchData();
expect(data).toBe('peanut butter');
});
test('the fetch fails with an error', async () => {
expect.assertions(1);
try {
await fetchData();
} catch (e) {
expect(e).toMatch('error');
}
});
同样,在async
函数中也可以使用resolves
和rejects
方法
test('the data is peanut butter', async () => {
expect.assertions(1);
await expect(fetchData()).resolves.toBe('peanut butter');
});
test('the fetch fails with an error', async () => {
expect.assertions(1);
await expect(fetchData()).rejects.toMatch('error');
});
测试准备和测后整理
辅助函数
Jest提供了辅助函数帮助我们在运行测试前做一些准备工作,在测试结束后进行整理工作:
beforeEach
和afterEach
:在每个测试前/后执行beforeAll
和afterAll
:在文件前/后执行- 当
before
和after
的块在describe
块内部时,就只适用于该describe
块内的测试
desribe
和test
块的执行顺序
Jest会在真正的测试(test
)开始之前执行测试文件里所有的describe
处理程序,当describe
运行后会按照test
出现的顺序运行所有测试
(可以把每个test
当做异步任务看待)
describe('outer', () => {
console.log('describe outer-a');
describe('describe inner 1', () => {
console.log('describe inner 1');
test('test 1', () => {
console.log('test for describe inner 1');
expect(true).toEqual(true);
});
});
console.log('describe outer-b');
test('test 1', () => {
console.log('test for describe outer');
expect(true).toEqual(true);
});
describe('describe inner 2', () => {
console.log('describe inner 2');
test('test for describe inner 2', () => {
console.log('test for describe inner 2');
expect(false).toEqual(false);
});
});
console.log('describe outer-c');
});
// describe outer-a
// describe inner 1
// describe outer-b
// describe inner 2
// describe outer-c
// test for describe inner 1
// test for describe outer
// test for describe inner 2
单独测试
如果测试失败,可以使用test.only
来单独运行一个测试:
test.only('this will be the only test that runs', () => {
expect(true).toBe(false);
});
test('this test will not run', () => {
expect('A').toBe('A');
});
Mock Function
Mock函数用来模拟函数、方法的外部依赖,让我们清除无关的影响,专注于被测代码本身
有两种方式可以mock函数:
1. 在测试代码中创建一个mock函数
2. 自己编写一个manual mock
来覆盖模块之间的依赖
使用Mock函数
创建一个mock
函数很简单:
const mockCallback = jest.fn();
mock函数一些重要的API:
mockFn.mockName(value)
:为jest.fn()
设定一个名称,用于输出时辨识使用mockFn.getMockName
:获取使用mockFn.mockName
设定的mock函数名mockFn.mock
:所有的mock函数都有一个特殊的属性.mock
,它保存了次函数被调用的信息,并且追踪了每次调用时this
的值mockFn.mock.calls
:函数被调用时传入的参数数组组成的二维数组,比如函数fn()
被调用了两次,第一次是fn(arg1, arg2)
,第二次是fn(arg3,arg4)
,这时fn.mock.calls
就是[[arg1, arg2], [arg3, arg4]]
mockFn.mock.instances
:函数通过new
关键字被实例化成员组成的数组,例如:const mockFn = jest.fn(); const a = new mockFn(); const b = new mockFn(); mockFn.mock.instances[0] === a; // true mockFn.mock.instances[1] === b; // true
mockFn.mockReturnValue(value)
:接受一个参数,在mock函数被调用时总被作为返回值返回mockFn.mockReturnValue(value)
:接受一个参数,在mock函数被调用时作为返回值返回一次mockFn.mockReturnThis()
:语法糖,返回this
这样就可以使用mock函数进行测试:
function forEach(items, callback) {
for (let index = 0; index < items.length; index++) {
callback(items[index]);
}
}
const mockCallback = jest.fn();
forEach([0, 1], mockCallback);
// 此模拟函数被调用了两次
expect(mockCallback.mock.calls.length).toBe(2);
// 第一次调用函数时的第一个参数是 0
expect(mockCallback.mock.calls[0][0]).toBe(0);
// 第二次调用函数时的第一个参数是 1
expect(mockCallback.mock.calls[1][0]).toBe(1);
Mock函数也可以用于在测试期间将测试值注入您的代码︰
const myMock = jest.fn();
console.log(myMock());
// > undefined
myMock
.mockReturnValueOnce(10)
.mockReturnValueOnce('x')
.mockReturnValue(true);
console.log(myMock(), myMock(), myMock(), myMock());
// > 10, 'x', true, true
mock函数非常适用于函数式编程的代码中:
const filterTestFn = jest.fn();
// Make the mock return `true` for the first call,
// and `false` for the second call
filterTestFn.mockReturnValueOnce(true).mockReturnValueOnce(false);
const result = [11, 12].filter(filterTestFn);
console.log(result);
// > [11]
console.log(filterTestFn.mock.calls);
// > [ [11], [12] ]
mock实现
有时候mock函数不光要记录被调用的情况,还需要执行一些特定的操作,可以通过mockImplementation
实现(或者直接向jest.fn()
中传入一个函数作为参数)
const mockFn = jest.fn().mockImplementation(scalar => 42 + scalar);
// or: jest.fn(scalar => 42 + scalar);
const a = mockFn(0);
const b = mockFn(1);
a === 42; // true
b === 43; // true
mockFn.mock.calls[0][0] === 0; // true
mockFn.mock.calls[1][0] === 1; // true
有时候函数是从其他的模块中引入的,我们也需要将其mock掉:
// foo.js
module.exports = function() {
// some implementation;
};
// test.js
jest.mock('../foo'); // this happens automatically with automocking
const foo = require('../foo');
// foo is a mock function
foo.mockImplementation(() => 42);
foo();
// > 42
当被模拟的函数运行完了mockImplementationOnce
定义的实现时,它将执行jest.fn
(如果被定义了)的默认实现:
const myMockFn = jest
.fn(() => 'default')
.mockImplementationOnce(() => 'first call')
.mockImplementationOnce(() => 'second call');
console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn());
// > 'first call', 'second call', 'default', 'default'
对于我们有通常链接的方法(因此总是需要返回this
)的情况,我们有一个语法,API以.mockReturnThis()
函数的形式来简化它,它也位于所有模拟器上:
const myObj = {
myMethod: jest.fn().mockReturnThis(),
};
// is the same as
const otherObj = {
myMethod: jest.fn(function() {
return this;
}),
};
mock函数的匹配器
Jest为我们提供了一些mock函数的匹配器
// 这个 mock 函数至少被调用一次
expect(mockFunc).toBeCalled();
// 这个 mock 函数至少被调用一次,而且传入了特定参数
expect(mockFunc).toBeCalledWith(arg1, arg2);
// 这个 mock 函数的最后一次调用传入了特定参数
expect(mockFunc).lastCalledWith(arg1, arg2);
// 所有的 mock 的调用和名称都被写入了快照
expect(mockFunc).toMatchSnapshot();
这些都是语法糖,我们都可以通过mockFn.mock
属性获取到:
// 这个 mock 函数至少被调用一次
expect(mockFunc.mock.calls.length).toBeGreaterThan(0);
// 这个 mock 函数至少被调用一次,而且传入了特定参数
expect(mockFunc.mock.calls).toContain([arg1, arg2]);
// 这个 mock 函数的最后一次调用传入了特定参数
expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1]).toEqual([
arg1,
arg2,
]);
// 这个 mock 函数的最后一次调用的第一个参数是`42`
// (注意这个断言的规范是没有语法糖的)
expect(mockFunc.mock.calls[mockFunc.mock.calls.length - 1][0]).toBe(42);
// 快照会检查 mock 函数被调用了同样的次数,
// 同样的顺序,和同样的参数 它还会在名称上断言。
expect(mockFunc.mock.calls).toEqual([[arg1, arg2]]);
expect(mockFunc.mock.getMockName()).toBe('a mock name');
在Vue中配置Jest
自动配置
通过Vue-cli
创造了模板脚手架时,可以选择是否启用单元测试,并且选择单元测试框架,这样Vue就帮助我们自动配置好了Jest。
手动配置
如果需要手动配置的话,首先要安装Jest和Vue Test Utils
npm install --save-dev jest @vue/test-utils
然后在package.json
中定义一个单元测试的脚本。
// package.json
{
"scripts": {
"test": "jest"
}
}
为了告诉Jest如何处理*.vue
文件,需要安装和配置vue-jest
预处理器:
npm install --save-dev vue-jest
接下来在package.json
中创建一个jest
块:
{
// ...
"jest": {
"moduleFileExtensions": [
"js",
"json",
// 告诉 Jest 处理 `*.vue` 文件
"vue"
],
"transform": {
// 用 `vue-jest` 处理 `*.vue` 文件
".*\\.(vue)$": "<rootDir>/node_modules/vue-jest"
}
}
}
在自动配置的时候,这部分内容是作为test
文件夹下jest.conf.js
配置文件单独出现的,在启动单元测的时候指定了这个配置文件:
"scripts": {
"unit": "jest --config test/unit/jest.conf.js --coverage",
"test": "npm run unit",
},
注意:
vue-jest
目前并不支持vue-loader
所有的功能,比如自定义块和样式加载。额外的,诸如代码分隔等 webpack 特有的功能也是不支持的。如果要使用这些不支持的特性,你需要用 Mocha 取代 Jest 来运行你的测试,同时用 webpack 来编译你的组件。
如果在webpack中配置了别名解析,比如把@
设置为/src
的别名,那么你也需要用moduleNameMapper
选项为Jest增加一个匹配配置:
{
// ...
"jest": {
// ...
// 支持源代码中相同的 `@` -> `src` 别名
"moduleNameMapper": {
"^@/(.*)$": "<rootDir>/src/$1"
}
}
}
尽管最新版本的Node已经支持绝大多数的ES2015特性,你可能仍然想要在你的测试中使用ES modules语法和stage-x的特性。为此我们需要安装 babel-jest
:
npm install --save-dev babel-jest
接下来,需要在package.json
的jest.transform
里添加一个入口,来告诉Jest用babel-jest
处理JavaScript测试文件:(同样可以在jest.conf.js
进行配置)
假设webpack使用了babel-preset-env
,这时默认的Babel配置会关闭ES modules
的转译,因为webpack已经可以处理ES modules
了。然而,我们还是需要为我们的测试而开启它,因为Jest的测试用例会直接运行在Node上。
同样的,我们可以告诉babel-preset-env
面向我们使用的Node版本。这样做会跳过转译不必要的特性使得测试启动更快。
了仅在测试时应用这些选项,可以把它们放到一个独立的 env.test 配置项中 (这会被babel-jest
自动获取)。
.babelrc
文件示例:
{
"presets": [
["env", { "modules": false }]
],
"env": {
"test": {
"presets": [
["env", { "targets": { "node": "current" }}]
]
}
}
}
测试覆盖率
Jest可以用来生成多种格式的测试覆盖率报告,通过扩展jest
配置通常在package.json
或jest.config.js
中) 的collectCoverage
选项,然后添加collectCoverageFrom
数组来定义需要收集测试覆盖率信息的文件。
{
"jest": {
// ...
"collectCoverage": true,
"collectCoverageFrom": [
"**/*.{js,vue}",
"!**/node_modules/**"
]
}
}
这样就会开启默认格式的测试覆盖率报告。你可以通过coverageReporters
选项来定制它们。
{
"jest": {
// ...
"coverageReporters": ["html", "text-summary"]
}
}
collectCoverage
:配置测试覆盖率是否开启,默认是关闭的,应为会降低测试速度collectCoverageFrom
:配置收集测试覆盖率文件范围coverageReporters
:测试覆盖率输出形式,text
或text-summary
对应控制台的输出coverageDirectory
:测试覆盖率输出形式目录,配置为'<rootDir>/test/unit/coverage'