常见测试场景
匹配
toBe与toEqual
两者都是用来验证相等的断言,toBe常用来比较值是否相等,toEqual常用来比较引用类型是否等价,按照官方解释,toEqual会递归对比对象实例的所有属性,因此也被称作深度相等。
test('相等', () => {
const foo = { bar: 1 };
expect(foo.bar).toBe(1); // 通过
expect(foo).toBe({ bar: 1 }); // 不通过
expect(foo).toEqual({ bar: 1 }); // 通过
});
not
not修饰符用来表示取反,用在其他断言之前,比如
test('not', () => {
const foo = 1;
expect(foo).not.toBe(2); // 通过
});
toMatch
toMatch允许我们传入一个正则表达式,用来精确匹配字符串。
test('match', () => {
const foo = 'hello jest';
expect(foo).toMatch(/hello/i); // 通过
});
toContain
toContain用来检测对象中是否包含某个值。
test('contain', () => {
const data = ['foo', 'bar'];
expect(data).toContain('fo'); // 不通过
});
其他匹配器
·toBeNull:是否 null
· toBeUndefined:是否 undefined
· toBeDefined:是否定义
· toBeTruthy:是否为真
· toBeFalsy:是否为假
· toBeGreaterThan 大于
· toBeGreaterThanOrEqual 大于等于
· toBeLessThan 小于
· toBeLessThanOrEqual 小于等于
· toBeCloseTo 匹配浮点数
函数
测试异常
可以使用toThrow来测试函数执行过程中是否抛出错误,需要注意的是,在Jest中我们必须对被测试函数再做一层包装才有效。
function onlyNumber(param) {
if (typeof param !== 'number') {
throw Error('Only Number!');
}
return param;
}
test('throw', () => {
expect(onlyNumber('1')).toThrow('Only Number!'); // 通过
});
测试回调
关键是需要手动调用done()。
function getName(callback) {
new Promise((resolve) => {
setTimeout(() => {
resolve('bar');
}, 1000);
}).then((res) => {
callback(res);
});
}
describe('test callback', () => {
// 用例1, 错误用法
test('fault', () => {
getName((res) => {
expect(res).toBe('foo'); // 通过
});
});
// 用例2, 正确用法
test('right', (done) => {
getName((res) => {
expect(res).toBe('foo'); // 不通过
done();
});
});
});
在错误的用例1中,无论断言是toBe什么,只要被测试函数本身不报错,测试用例都会通过,因为回调函数本身并未执行。
需要在回调函数中手动调用done(),表示该回调函数执行以后,用例才算通过。
测试异步
这块测试我们可以通过使用 async 和 await 关键字来做到和写法和同步代码基本一致(推荐),也可以使用jest官方提供的resolves和rejects修饰符,当然,使用.then之类的链式写法也可以。后两种方法记得return。
// 模拟一个异步操作
function getName() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('bar');
}, 1000);
});
}
describe('test promise', () => {
// 写法1
test('style1', async () => {
const res = await getName();
expect(res).toBe('bar');
});
// 写法2
test('style2', () => {
return expect(getName()).resolves.toBe('bar');
});
// 写法3
test('style3', () => {
return getName().then((res) => {
expect(res).toBe('bar');
})
});
});
如果在测试时发现jest不认识async、await等关键字,那就需要加强下babel,首先安装 @babel/plugin-transform-runtime。
npm install --save-dev @babel/plugin-transform-runtime
配置babel
{
"plugins": ["@babel/plugin-transform-runtime"]
}
Mock
mock函数
mock函数常用于被测试内部结构复杂的函数的运行是否符合预期,mock函数就是jest提供的一个被监控的函数,函数的参数、返回值、函数体、被调用次数等一切属性都可以自定义或观察。
// 创建 mock 函数
const func = jest.fn();
// mock 函数返回值, 不 mock 默认 undefined
func.mockReturnValue(1);
// mock 函数返回值, 只生效一次,需要多次使用,方便 mock 不同情况下的返回值
func.mockReturnValueOnce(1);
// mock 函数实现
func.mockImplementation((params) => {
console.log('func be called with', params);
});
// mock 函数实现,只生效一次
func.mockImplementationOnce(() => {});
// mock 函数被调用次数
console.log(func.mock.calls.length);
// mock 函数第 i + 1 次被调用时所接受的实参
let i = 2;
console.log(func.mock.calls[i]);
// mock 函数第 i + 1 次被调用时内部的 this 指向
console.log(func.mock.instances[i]);
具体的例子在mock定时器一节中。
mock定时器
在实际的被测试代码中,可能会遇到代码中存在计时器的延时操作。假如这个时间是30s,那么我们在对这块代码做测试时一定不想等待30s。因此,jest提供了一些与计时器有关的API来帮助我们跳过或加速时间。
// 被测试代码
function handleData(callback) {
setTimeout(() => {
callback('bar');
seTimeout(() => {
callback('foo');
}, 1000);
}, 30 * 1000);
}
// 测试代码
// 每个测试用例都 mock 一个定时器, 避免串扰
beforeEach(() => { // beforeEach是jest提供的生命周期钩子, 每个测试用例执行前触发
jest.useFakeTimers(); // 一定要在 mock 计时器前进行声明
});
describe('test mock timer', () => {
test('runAllTimers', () => {
// mock 一个 callback 来用,
// 因为在本次测试中我们只关心计时器, 具体的内部操作我们不关心,
// 因此很适合用mock函数, 这样做的好处是假如内部是个耗时操作, 我们可以直接规避
const fn = jest.fn();
// 执行被测试代码
handleData(fn);
// 一次性执行完所有计时器
jest.runAllTimers();
// 回调执行了2次
expect(fn.mock.calls.length).toBe(2); // 通过
// 2次实参分别是'bar'和'foo'
expect(fn.mock.calls).toEqual(['bar', 'foo']); // 通过
});
test('runOnlyPendingTimers', () => {
const fn = jest.fn();
handleData(fn);
// 仅执行处于消息队列中的计时器
jest.runOnlyPendingTimers();
// 显然回调只会执行外层的一次
expect(fn.mock.calls.length).toBe(1); // 通过
});
test('advanceTimersByTime', () => {
const fn = jest.fn();
handleData(fn);
// 快进30s, 执行完外层计时器
jest.advanceTimersByTime(30 * 1000);
expect(fn.mock.calls.length).toBe(1); // 通过
// 快进1s, 执行完内层计时器
jest.advanceTimersByTime(1000);
expect(fn.mock.calls.length).toBe(2); // 通过
});
});
mock模块
我们在代码中总是会引用到第三方模块,如果我们要测试的某一段代码依赖某个第三方模块,我们此时就可以mock它。最常见的例子就是网络请求。要mock一个模块,我们可以在被测模块的同级目录下创建一个__mocks__文件夹,并且在该文件下创建同名的mock模块;也可以不用这么做,直接在测试文件中导入原有模块,用jest.mock来mock想mock的模块。
// 被mock模块
import axios from 'axios';
function fetchData1() {
return axios.get('https://xxx.xxx.xxx/xxx', res => res.data);
}
function fetchData2() {
return axios.get('https://xxx.xxx.xxx/xxx', res => res.data);
// 预计返回
// { type: '5G' }
}
export { fechData1, fetchData2 };
// mock 指定路径下的模块
jest.mock('./service', () => {
// 导入真实模块内容
const actualModules = jest.requireActual('./service');
// 混入 mock 内容
return {
...actualModules,
fetchData1: jest.fn(() => {
return new Promise((resolve, reject) => {
resolve({
foo: 'bar',
});
});
}),
};
});
// 此时导入的 fetchData1 是我们 mock 的, fetchData2 是原有的
import { fetchData1, fetchData2 } from './service';
describe('test mock modules', () => {
test('fetchData1', async () => {
const res = await fetchData1();
expect(res).toEqual({foo: 'bar'}); // 通过
});
test('fetchData2', async () => {
const res = await fechData2();
expect(res).toEqual({type: '5G'}); // 预期通过
});
});
本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理