零配置测试平台
Jest 被 Facebook 用来测试包括 React 应用在内的所有 JavaScript 代码。Jest 的一个理念是提供一套完整集成的 “零配置” 测试体验。
高速和沙盒
Jest 跨工人以最大化性能并行化的测试运行。控制台消息都是缓冲并输出测试结果。沙盒测试文件和自动全局状态将为每个测试重置,因此测试代码间不会冲突。
内置代码覆盖率报告
使用——coverage轻松创建代码覆盖率报告。不需要额外的设置或库!Jest可以从整个项目收集代码覆盖信息,包括未测试的文件。
无需配置
在你使用 create-react-app 或 react-native init 创建你的 React 或 React Native 项目时,Jest 都已经被配置好并可以使用了。在 __tests__文件夹下放置你的测试用例,或者使用 .spec.js 或 .test.js 后缀给它们命名。不管你选哪一种方式,Jest 都能找到并且运行它们。
安装:
yarn add --dev jest
或者 npm install --save-dev jest
开始编写第一个测试用例:
在项目 Test
目录里面创建一个 math.js
的文件:
export function sqrt(x) {
if (x < 0) throw new Error("负值没有平方根");
return Math.exp(Math.log(x)/2);
}
然后同级目录下面创建 math.test.js
的文件:
import {sqrt} from './math';
describe('main', function () {
test('4的平方根等于2', function () {
expect(sqrt(4)).toBe(2)
});
});
将下面的配置部分添加到你的 package.json
里面:
{
"scripts": {
"test": "jest"
}
}
最后,运行 npm run test, Jest将打印以下消息
PASS Test/math.test.js
main
√ 4的平方根等于2 (3ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 2.031s
匹配器就类似于断言语句,接受了期望函数之后, 就可以书写对期望的匹配了。
常用匹配器有如下:
expect({a:1}).toBe({a:1})//判断两个对象是否相等
expect(n).toEqual();//同上toBe()
expect(1).not.toBe(2)//判断不等
expect(n).toBeNull(); //判断是否为null
expect(n).toBeUndefined(); //判断是否为undefined
expect(n).toBeDefined(); //判断结果与toBeUndefined相反
expect(n).toBeTruthy(); //判断结果为true
expect(n).toBeFalsy(); //判断结果为false
expect(value).toBeGreaterThan(3); //大于3
expect(value).toBeGreaterThanOrEqual(3.5); //大于等于3.5
expect(value).toBeLessThan(5); //小于5
expect(value).toBeLessThanOrEqual(4.5); //小于等于4.5
expect(value).toBeCloseTo(0.3); // 浮点数判断相等
expect('Christoph').toMatch(/stop/); //正则表达式判断
expect(['one','two']).toContain('one'); //不解释
function compileAndroidCode() {
throw new ConfigError('you are using the wrong JDK');
}
test('compiling android goes as expected', () => {
expect(compileAndroidCode).toThrow();
expect(compileAndroidCode).toThrow(ConfigError); //判断抛出异常
})
异步测试的解决方式其实跟mocha 的解决方案是一致的。
使用单个参数调用 done,而不是将测试放在一个空参数的函数。 Jest会等done回调函数执行结束后,结束测试。
来举一个例子:
test('async function run err', function () {
let x = true;
let f = function() {
x = false;
expect(x).toBeTruthy();
};
setTimeout(f, 4000);
})
上面这个例子将会永远成功, 因为我们异步执行的时候, 还没有执行到f方法, 测试用例已经执行完毕了, 也就是说, 测试执行完毕的时候, 压根就还没执行到f方法。
当我们加上done()之后, 将会成为另外一件事儿了, done()之后, 是一定要执行了done()才算是整个test单元执行完毕, 否则就会永远挂起。 这个地方简介也引出了一个需要注意的问题, 就是如果有了test测试单元回调函数有了done参数, done(), 就一定要执行, 否则测试就会挂起。
test('async function run success', function (done) {
let x = true;
let f = function() {
x = false;
expect(x).toBeTruthy();
done();
};
setTimeout(f, 4000);
})
上面示例将会抛错, 原因就是执行了异步函数方法 f, 然后将 x改为false了, 然后匹配器 匹配的确实true, 所以代码报错。
Promise
Jest是内置Promise的测试的, 测试返回一个 Promise, Jest 会等待这一 Promise 来解决。 但是有几个问题需要注意:
- 如果承诺被拒绝,则测试将自动失败 (一定要有return)
test('Promise testing resolve', function () {
let timer = +new Date();
let flag = timer % 2 === 1 ? true : false;
let returnPromise = function () {
return new Promise((resolve, reject) => {
if (flag) {
resolve(flag)
} else {
reject(flag)
}
});
};
return returnPromise()
.then(flag => {
expect(flag).toBeFalsy();
})
})
- 如果你想要 Promise 被拒绝,使用 .catch 方法。 请确保添加 expect.assertions 来验证一定数量的断言被调用。
test('Promise testing reject', function () {
let timer = +new Date();
let flag = timer % 2 === 1 ? true : false;
let returnPromise = function () {
return new Promise((resolve, reject) => {
if (flag) {
resolve(flag)
} else {
reject(flag)
}
});
};
return returnPromise()
.catch(flag => {
expect.assertions(1);
expect(flag).tobeFalsy();
})
});
如果不加 expect.assertions(1); 下面的断言 expect(flag).tobeFalsy();
是不成立的。
使用.resolves/.rejects 的Promise测试
可以直接使用 .resolves/.rejects
来匹配自己想要的Promise状态, 而且Jest也会等待 Promise的状态返回。
test('Promise testing used .resolves and .rejects', function () {
let timer = +new Date();
let flag = timer % 2 === 1 ? true : false;
let returnPromise = function () {
return new Promise((resolve, reject) => {
if (flag) {
resolve(flag)
} else {
reject(flag)
}
});
};
return expect(returnPromise()).resolves.toBeTruthy();
});
如果是 catch 里面去了, 也会直接报错, 如果是成功的返回, 就会走断言的判断结果。
同理, 如果是判断rejected 状态, 直接用 .rejectes
就可以了。
async/await:
Jest同样是支持 async/await
的, 写法可以参考下面
test('async/await testing', async function () {
let timer = +new Date();
let flag = timer % 2 === 1 ? true : false;
let returnPromise = function () {
return new Promise((resolve, reject) => {
if (flag) {
resolve(flag)
} else {
reject(flag)
}
});
};
let result = await returnPromise();
expect(result).toBeTruthy();
})
对于对于Promise
场景, 也可以直接使用async/await
;
test('Promise testing used .resolves and .rejects', async function () {
let timer = +new Date();
let flag = timer % 2 === 1 ? true : false;
let returnPromise = function () {
return new Promise((resolve, reject) => {
if (flag) {
resolve(flag)
} else {
reject(flag)
}
});
};
await expect(returnPromise()).resolves.toBeTruthy();
});
写测试的时候你经常需要在运行测试前做一些准备工作,和在运行测试后进行一些整理工作。 Jest 提供辅助函数来处理这个问题。
为多次测试重复设置
使用 beforeEach
和 afterEach
; 分别表示每次test执行之前和执行之后, 都要执行的函数示例, 这两个作为设置函数, 接受一个回调函数作为参数。
其内部同样也可以使用异步函数, 但是要记得return;
例如我们有下面这样的示例例子:
describe('Setup and Teardown', function () {
beforeEach(function () {
console.log('beforeEach')
});
afterEach(function () {
console.log('afterEach')
});
test('match string one', function () {
expect('yanlele').toMatch('yan');
});
test('match string two', function () {
expect('yanlele').toMatch('lele');
})
});
输出的日志结果如下:
Setup and Teardown
console.log Test/01、基础部分/03、setupAndTeardown.test.js:8
beforeEach
console.log Test/01、基础部分/03、setupAndTeardown.test.js:12
afterEach
console.log Test/01、基础部分/03、setupAndTeardown.test.js:8
beforeEach
console.log Test/01、基础部分/03、setupAndTeardown.test.js:12
afterEach
✓ match string one (78ms)
✓ match string two (3ms)
一次性设置:
有的情况是只需要在一个test文件中所有测试用例或者用例集执行之前或者之后执行, 而且只执行一次,这个时候就要用到 beforeAll
和 afterAll
。
用法与上面的beforeEach
和 afterEach
是一样的, 这里就不做演示了。
只执行一个测试关键字only
如果只希望执行一个测试用例, 那么直接使用test.only('Description', Function);
就可以了, 这个地方跟mocha是一模一样的。
Jest 中的三个与 Mock 函数相关的API,分别是jest.fn()、jest.spyOn()、jest.mock()。 使用它们创建Mock函数能够帮助我们更好的测试项目中一些逻辑较复杂的代码,例如测试函数的嵌套调用,回调函数的调用等。
Mock函数提供的以下三种特性,在我们写测试代码时十分有用:
- 捕获函数调用情况
- 设置函数返回值
- 改变函数的内部实现
jest.fn()是�创建Mock函数最简单的方式,如果没有定义函数内部的实现,jest.fn()会返回undefined作为返回值。
test('测试jest.fn()调用', () => {
let mockFn = jest.fn();
mockFn(1, 2, 3);
mockFn(1, 2, 3);
let result = mockFn(1, 2, 3);
// 断言mockFn的执行后返回undefined
expect(result).toBeUndefined();
// 断言mockFn被调用
expect(mockFn).toBeCalled();
// 断言mockFn被调用了一次
expect(mockFn).toBeCalledTimes(3);
// 断言mockFn传入的参数为1, 2, 3
expect(mockFn).toHaveBeenCalledWith(1, 2, 3);
});
jest.fn()所创建的Mock函数还可以设置返回值,定义内部实现或返回Promise对象。
test('测试jest.fn()返回固定值', () => {
let mockFn = jest.fn().mockReturnValue('default');
// 断言mockFn执行后返回值为default
expect(mockFn()).toBe('default');
});
test('测试�jest.fn()内部实现', () => {
let mockFn = jest.fn((num1, num2) => {
return num1 * num2;
});
// 断言mockFn执行后返回100
expect(mockFn(10, 10)).toBe(100);
});
test('测试jest.fn()返回Promise', async () => {
let mockFn = jest.fn().mockResolvedValue('default');
let result = await mockFn();
// 断言mockFn通过await关键字执行后返回值为default
expect(result).toBe('default');
// 断言mockFn调用后返回的是Promise对象
expect(Object.prototype.toString.call(mockFn())).toBe("[object Promise]");
});
上面的代码是jest.fn()提供的几个常用的API和断言语句,下面我们在src/fetch.js文件中写一些被测试代码,
以更加接近业务的方式来理解Mock函数的实际应用。
被测试代码中依赖了axios这个常用的请求库和JSONPlaceholder这个上篇文章中提到免费的请求接口��,
请先在shell中执行npm install axios --save安装依赖。
// fetch.js
import axios from 'axios';
export default {
async fetchPostsList(callback) {
return axios.get('https://jsonplaceholder.typicode.com/posts').then(res => {
return callback(res.data);
})
}
}
我们在fetch.js中封装了一个fetchPostsList方法,该方法请求了JSONPlaceholder提供的接口, 并通过传入的回调函数返回处理过的返回值。如果我们想测试该接口能够被正常请求, 只需要捕获到传入的回调函数能够被正常的调用即可。下面是functions.test.js中的测试的代码。
import fetch from '../src/fetch.js'
test('fetchPostsList中的回调函数应该能够被调用', async () => {
expect.assertions(1);
let mockFn = jest.fn();
await fetch.fetchPostsList(mockFn);
// 断言mockFn被调用
expect(mockFn).toBeCalled();
});
fetch.js文件夹中封装的请求方法可能我们在其他模块被调用的时候,并不需要进行实际的请求(请求方法已经通过单侧或需要该方法返回非真实数据)。 此时,使用jest.mock()去mock整个模块是十分有必要的。
下面我们在util/fetch.js的同级目录下创建一个util/events.js。
import fetch from './fetch'
export default {
async getPostList() {
return fetch.fetchPostsList(data => {
console.log('fetchPostsList be called');
})
}
}
在 05、jest.mock.test.js 写下如下测试代码
import events from "../util/events";
import fetch from "../util/fetch";
jest.mock('../util/fetch.js');
test('mock 整个 fetch.js模块', async () => {
expect.assertions(2);
await events.getPostList();
expect(fetch.fetchPostsList).toHaveBeenCalled();
expect(fetch.fetchPostsList).toHaveBeenCalledTimes(1);
}, 10000);
这个地方需要注意的是, jest.mock() 要写在外层; 测试代码中我们使用了jest.mock('../src/fetch.js')去mock整个fetch.js模块。如果注释掉这行代码,执行测试脚本时会出现以下报错信息
Error: expect(jest.fn())[.not].toHaveBeenCalled()
jest.fn() value must be a mock function or spy.
Received:
function: [Function fetchPostsList]
55 | expect.assertions(2);
56 | await events.getPostList();
> 57 | expect(fetch.fetchPostsList).toHaveBeenCalled();
| ^
58 | expect(fetch.fetchPostsList).toHaveBeenCalledTimes(1);
59 | }, 10000);
60 | });
从这个报错中,我们可以总结出一个重要的结论:
在jest中如果想捕获函数的调用情况,则该函数必须被mock或者spy!
jest.spyOn()方法同样创建一个mock函数,但是该mock函数不仅能够捕获函数的调用情况,还可以正常的执行被spy的函数。
实际上,jest.spyOn()是jest.fn()的语法糖,它创建了一个和被spy的函数具有相同内部代码的mock函数。
上图是之前jest.mock()的示例代码中的正确执行结果的截图, 从shell脚本中可以看到console.log('fetchPostsList be called!'); 这行代码并没有在shell中被打印,这是因为通过jest.mock()后,模块内的方法是不会被jest所实际执行的。 这时我们就需要使用jest.spyOn()。
it('使用jest.spyOn()监控fetch.fetchPostsList被正常调用', async () => {
expect.assertions(2);
const spyFn = jest.spyOn(fetch, 'fetchPostsList');
await events.getPostList();
expect(spyFn).toHaveBeenCalled();
expect(spyFn).toHaveBeenCalledTimes(1);
})
这篇文章中我们介绍了jest.fn(),jest.mock()和jest.spyOn()来创建mock函数, 通过mock函数我们可以通过以下三个特性去更好的编写我们的测试代码:
- 捕获函数调用情况
- 设置函数返回值
- 改变函数的内部实现 在实际项目的单元测试中,jest.fn()常被用来进行某些有回调函数的测试; jest.mock()可以mock整个模块中的方法,当某个模块已经被单元测试100%覆盖时,使用jest.mock()去mock该模块,节约测试时间和测试的冗余度是十分必要; 当需要测试某些必须被完整执行的方法时,常常需要使用jest.spyOn()。这些都需要开发者根据实际的业务代码灵活选择。