Jest 文档学习
文章内容主要来自官方文档的摘要以及个人理解
参考文章:
快速开始
首先需要安装Jest
npm install --save-dev jest
或
yarn add --dev jest
运行npx jest --init生成配置文件;如果希望支持Typescript,可以安装以下依赖,运行结束后会自动生成配置文件
yarn add --dev ts-jest @types/jest
yarn ts-jest config:init
运行npm i babel-jest @babel/core @babel/preset-env -D安装babel,并且配置.babelrc如下
Would you like to use Typescript for the configuration file? ... yes
编写需要测试的代码(以下是两数之和的函数)
function sum(a, b) {
return a + b;
}
module.exports = sum;
如果对这个函数写一个测试其正确是否的测试程序,它可能构思是这这样的:
期望 sum(1,2) 的结果是3
进一步转化为英文
expect sum(1,2) to be 3
用程序性的语言表示,expect作为一个函数,为它传入想要测试的对象(sum函数),把输出结果也做一层封装toBe(3)
expect(sum(1,2)).toBe(3)
更进一步,添加描述信息。我们可以再做一层封装,定义一个test函数,它有两个参数,第一个参数是一些描述性信息(这里是 测试sum函数),第二个参数是一个函数,函数里可以执行我们上面的逻辑,如下
test("1 + 2 = 3", () => {
expect(sum(1,2)).toBe(3)
})
Matchers 匹配器
用于匹配测试程序结果的方法被称之为matcher,即在expect函数后面跟着的判断结果的toBe之类的方法
常见匹配器
toBe
toBe使用Object.is()来进行精准匹配
Object.is() 方法判断两个值是否为同一个值,如果满足以下任意条件则两个值相等:
toEqual toStrictEqual
检查对象的值,而不是引用地址
test('对象赋值', () => {
const data = {one: 1};
data['two'] = 2;
expect(data).toEqual({one: 1, two: 2});
});
toEqual递归查找对象或数组的每个字段
提示 使用
toStrictEqual优于使用toEqual。toEqual只是简单忽略undefined值,而toStictEqual则考虑它们
not
我们还可以使用与匹配相反的 not 来进行测试
test('adding positive numbers is not zero', () => {
for (let a = 1; a < 10; a++) {
for (let b = 1; b < 10; b++) {
expect(a + b).not.toBe(0);
}
}
});
真值
代码中的undefined, null, and false有不同含义,若你在测试时不想区分他们,可以用真值判断。 Jest提供helpers供你使用。
toBeNull只匹配nulltoBeUndefined只匹配undefinedtoBeDefined与toBeUndefined相反toBeTruthy匹配任何if语句为真toBeFalsy匹配任何if语句为假
数字
test('two plus two', () => {
const value = 2 + 2;
// 大于
expect(value).toBeGreaterThan(3);
// 大于等于
expect(value).toBeGreaterThanOrEqual(3.5);
// 小于
expect(value).toBeLessThan(5);
// 小于等于
expect(value).toBeLessThanOrEqual(4.5);
});
对于比较浮点数相等,使用 toBeCloseTo 而不是 toEqual,因为你不希望测试取决于一个小小的舍入误差。
test('两个浮点数字相加', () => {
const value = 0.1 + 0.2;
//expect(value).toBe(0.3); 这句会报错,因为浮点数有舍入误差
expect(value).toBeCloseTo(0.3); // 这句可以运行
});
字符串
您可以检查对具有 toMatch 正则表达式的字符串︰
test('there is no I in team', () => {
expect('team').not.toMatch(/I/);
});
test('but there is a "stop" in Christoph', () => {
expect('Christoph').toMatch(/stop/);
});
错误
若你想测试某函数在调用时是否抛出了错误,你需要使用 toThrow。
function compileAndroidCode() {
throw new Error('you are using the wrong JDK!');
}
test('compiling android goes as expected', () => {
expect(() => compileAndroidCode()).toThrow();
expect(() => compileAndroidCode()).toThrow(Error);
// 你可以使用一个字符串或者正则表示错误信息中必须包含的文字
expect(() => compileAndroidCode()).toThrow('you are using the wrong JDK');
expect(() => compileAndroidCode()).toThrow(/JDK/);
// Or you can match an exact error message using a regexp like below
expect(() => compileAndroidCode()).toThrow(/^you are using the wrong JDK$/); // Test fails
expect(() => compileAndroidCode()).toThrow(/^you are using the wrong JDK!$/); // Test pass
});
测试异步代码
在JavaScript中执行异步代码是很常见的。 当你有以异步方式运行的代码时,Jest 需要知道当前它测试的代码是否已完成,然后它可以转移到另一个测试。 Jest有若干方法处理这种情况。
Promise
为你的测试返回一个Promise,Jest会等待Promise的状态,如果Promise的状态变为rejected, 测试将会失败
例如,有一个名为fetchData的Promise, 假设它会返回内容为'peanut butter'的字符串 我们可以使用下面的测试代码︰
test('the data is peanut butter', () => {
return fetchData().then(data => {
expect(data).toBe('peanut butter');
});
});
Async/Await
写异步测试用例时,可以在传递给test的函数前面加上async。 例如,可以用来测试相同的fetchData 方案︰
test('the data is peanut butter', async () => {
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');
}
});
准备与收尾
重复设置
如果你有一些要为多次测试重复设置的工作,你可以使用 beforeEach 和 afterEach,两者用法相同
beforeEach(fn, timeout)
文件内每个测试开始前执行的钩子函数。 如果传入的回调函数返回值是 promise 或者 generator,Jest 会等待 promise resolve 再继续执行测试。
可选地传入第二个参数 timeout(毫秒) 指定函数执行超时时间。
一次性设置
在某些情况下,你只需要在文件的开头做一次设置。 如果这个通用设置是异步的,就比较麻烦,因为没办法每个用例都设置一遍,这样性能还很差。Jest提供了beforeAll和afterAll
beforeAll(fn, timeout)
文件内所有测试开始前执行的钩子函数。 如果传入的回调函数返回值是 promise 或者 generator,Jest 会等待 promise resolve 再继续执行。
可选地传入第二个参数 timeout(毫秒) 指定函数执行超时时间.
使用 beforeAll 会非常方便你设置一些在测试用例之间共享的全局状态。
作用域
顶层的 before* 和 after* hook 函数会应用于文件中的每一条测试 在 describe 块中声明的 hook 函数,只会作用于 describe 中的测试
describe('matching cities to foods', () => {
// Applies only to tests in this describe block
beforeEach(() => {
return initializeFoodDatabase();
});
test('Vienna <3 veal', () => {
expect(isValidCityFoodPair('Vienna', 'Wiener Schnitzel')).toBe(true);
});
test('San Juan <3 plantains', () => {
expect(isValidCityFoodPair('San Juan', 'Mofongo')).toBe(true);
});
});
注意顶级的beforeEach 会比 describe 中的beforeEach 执行的更早。
模拟函数
Mock 函数允许你测试代码之间的连接——实现方式包括:擦除函数的实际实现、捕获对函数的调用 ( 以及在这些调用中传递的参数) 、在使用 new 实例化时捕获构造函数的实例、允许测试时配置返回值。
有两种方法可以模拟函数:要么在测试代码中创建一个 mock 函数,要么编写一个手动 mock来覆盖模块依赖。
使用mock函数
在项目中,经常会碰见A模块掉B模块的方法。并且,在单元测试中,我们可能并不需要关心内部调用的方法的执行过程和结果,只想知道它是否被正确调用即可,甚至会指定该函数的返回值。此时,就需要mock函数了。
Mock函数提供的以下三种特性,在我们写测试代码时十分有用:
- 捕获函数调用情况
- 设置函数返回值
- 改变函数的内部实现
jest.fn()
jest.fn()是创建Mock函数最简单的方式,如果没有定义函数内部的实现,jest.fn()会返回undefined作为返回值。
// functions.test.js
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);
})
jest.fn()可以初始化时候不传入参数,然后通过调用生成的mock函数的mockImplementation或者mockImplementationOnce方法来改变mock函数内容,这两个方法的区别是
mockImplementationOnce只会改变要mock的函数一次:
test('测试 jest.fn()', () => {
const func = jest.fn()
func.mockImplementation(() => {
return 'this is mock fn 1'
})
func.mockImplementationOnce(() => {
return 'this is mock fn 2'
})
const a = run(func)
const b = run(func)
const c = run(func)
console.log(a)
console.log(b)
console.log(c)
})
我们可以看到,函数执行的结果第一次是
this is mock fn 2,之后都是this is mock fn 1
同样的,我们可以使用mock函数的mockReturnValue和mockReturnValueOnce(一次)方法来改变函数的返回值:
test('测试 jest.fn()', () => {
const func = jest.fn()
func.mockImplementation(() => {
return 'this is mock fn 1'
})
func.mockImplementationOnce(() => {
return 'this is mock fn 2'
})
func.mockReturnValue('this is mock fn 3')
func.mockReturnValueOnce('this is mock fn 4')
.mockReturnValueOnce('this is mock fn 5')
.mockReturnValueOnce('this is mock fn 6')
const a = run(func)
const b = run(func)
const c = run(func)
const d = run(func)
console.log(a)
console.log(b)
console.log(c)
console.log(d)
})
// functions.test.js
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]");
})
.mock属性
所有的 mock 函数都有这个特殊的.mock属性,它保存了关于此函数如何被调用、调用时的返回值的信息。
.mock属性还追踪每次调用时this的值,所以我们同样可以也检视(inspect) this:
const myMock1 = jest.fn();
const a = new myMock1();
console.log(myMock1.mock.instances);
// > [ <a> ]
const myMock2 = jest.fn();
const b = {};
const bound = myMock2.bind(b);
bound();
console.log(myMock2.mock.contexts);
// > [ <b> ] 一个包含mock函数所有调用上下文的数组。
Mock的返回值
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
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(num => filterTestFn(num));
console.log(result);
// > [11]
console.log(filterTestFn.mock.calls[0][0]); // 11
console.log(filterTestFn.mock.calls[1][0]); // 12
模拟模块
假定有个从 API 获取用户的类。 该类用axios调用 API 然后返回 data,其中包含所有用户的属性:
import axios from 'axios';
class Users {
static all() {
return axios.get('/users.json').then(resp => resp.data);
}
}
export default Users;
现在,为测试该方法而不实际调用 API (使测试缓慢与脆弱),我们可以用 jest.mock(...) 函数自动模拟 axios 模块。
一旦模拟模块,我们可为 .get 提供一个 mockResolvedValue ,它会返回假数据用于测试。 实际上,我们想说的是我们想让axios.get('/users.json') 有个伪造的响应结果。
import axios from 'axios';
import Users from './users';
jest.mock('axios');
test('should fetch users', () => {
const users = [{name: 'Bob'}];
const resp = {data: users};
axios.get.mockResolvedValue(resp);
// or you could use the following depending on your use case:
// axios.get.mockImplementation(() => Promise.resolve(resp))
return Users.all().then(data => expect(data).toEqual(users));
});