jest的官方文档,对API进行了全面、详细的说明,本文并不是API文档,而是记录了自己在工作中常用的测试场景,可节省查找文档的时间。
1. mock 外部依赖
想要测试函数:filterFeishuDeptIds、filterFeishuIdByTag,它们中依赖了cacheUtils模块中的两个方法,这两个方法会去发起外部请求,测试时,不希望调用外部请求。
js
jest.mock('../../server/utils/cacheUtils', () => {
return {
getDeptFeishuMap: jest.fn(() => {
console.log('mock getDeptFeishuMap for recipients util');
return {
1: '1001',
2: '1002',
3: '1003',
4: '1004',
};
}),
getTagFeishuMap: jest.fn(() => {
console.log('mock getTagFeishuMap for recipients util');
return {
1: 2001,
2: 2002,
3: 2003,
4: 2004,
};
})
};
});
describe('filterFeishuDeptIds', function () {
it('过滤无效部门', async () => {
const ids = [1, 2, 50];
// 50不是有效部门id
const list = await filterFeishuDeptIds(ids);
expect(list.length).toBe(2);
const noItem = list.find(one => one.id === 50);
expect(noItem).toBe(undefined);
});
});
describe('filterFeishuIdByTag', function () {
it('过滤无效部门', async () => {
const ids = [1, 2, 60];
const list = await filterFeishuIdByTag(ids);
expect(list.length).toBe(2);
});
});
如果mock的这个模块中,有的方法是希望mock,但也有方法是希望能被真实调用,要怎么办?
js
jest.mock('../../server/utils/cacheUtils', () => {
// 1. 使用requireActual加载真正的模块
const originalModule = jest.requireActual('../../server/utils/cacheUtils');
return {
getDeptFeishuMap: jest.fn(() => {
console.log('mock getDeptFeishuMap for recipients util');
return {
1: '1001',
2: '1002',
3: '1003',
4: '1004',
};
}),
// 2. 此方法使用真正的方法
getTagFeishuMap: originalModule.getTagFeishuMap,
};
});
第二个用例未做调整,失败是正常的。
2. mock 类
演示中用到的Person
类
js
class Person{
constructor(name, age){
this.name = name;
this.age = age;
console.log('Person constructor');
}
get displayName(){
console.log('Person displayName');
return this.name + '(' + this.age + ')';
}
canRetire(){
console.log('Person canRetire');
return this.age >= Person.RetireAge;
}
static isAdult(age){
console.log('Person isAdult');
return age >= 18;
}
}
Person.RetireAge = 65;
module.exports = Person;
2.1 实例方法
对类使用mockImplementation
方法,实例方法为返回对象的方法
js
const Person = require('./Person');
// Person 是默认导出,第一个return 返回的是函数(Person)
// 如果非默认导出,则应该return {Person: jest.fn().xxx}
jest.mock('./Person', () => {
return jest.fn().mockImplementation(() => {
return {
// 实例防范
canRetire: jest.fn(() => {
console.log('mock canRetire');
return true;
}),
};
});
});
js
describe('Person', () => {
it('实例方法-canRetire', () => {
const person = new Person('橘子', 28);
console.log(Person);
console.log(person);
// 构造函数被调用的次数
expect(Person).toHaveBeenCalledTimes(1);
expect(person.canRetire()).toBe(true);
});
});
Person类为一个mock函数------源码中定义的静态属性、静态方法都不存在,person实例中只有mock的canRetire
方法,其他方法和属性也都不存在。
2.2 静态方法
在类对象上使用spyOn
方法
jest.spyOn()
方法创建一个mock函数,并且可以正常执行 被spy的函数。jest.spyOn()
是jest.fn()
的语法糖,它创建了一个和被spy的函数具有相同内部代码
的mock函数。
js
const Person = require('./Person');
// 静态方法
jest.spyOn(Person, 'isAdult').mockImplementation(() => {
console.log('mock isAdult');
return true;
});
describe('Person', () => {
it('静态方法-isAdult', () => {
const person = new Person('橘子', 28);
console.log(Person);
console.log(person);
expect(Person.isAdult(18)).toBe(true);
});
});
类本身没有被mock,只有spyOn
静态方法被mock,实例也没被mock。
对类的prototype
属性用spyOn
同样可以mock实例方法
js
jest.spyOn(Person.prototype, 'canRetire').mockkImplementation(() => {
console.log('mock canRetire by spyOn');
return false;
});
describe('Person', () => {
it('实例方法-canRetire', () => {
const person = new Person('橘子', 28);
expect(person.canRetire()).toBe(false);
});
});
如果在同一个文件中要同时mock静态方法、实例方法,推荐使用spyOn
。
2.3 实例属性的getter、setter
js
jest.spyOn(Person.prototype, 'displayName', 'get').mockImplementation(() => {
console.log('mock get displayName');
return 'test(18)';
});
describe('Person', () => {
it('属性的getter', () => {
const person = new Person('橘子', 28);
expect(person.displayName).toBe('test(18)');
});
});
3. koa 中间件测试
中间件中有ctx、next对象,如果全部要mock,比较麻烦,可以使用supertest
。
需要测试的中间件如下(具体逻辑不重要,中间件就行):
a) 创建 koa app对象
js
/**
* 此文件是为了单测而存在的,不需要监听端口
*/
import Koa from 'koa';
import Router from 'koa-router';
const app = new Koa();
// 定义路由
const router = new Router();
router.get('/', async ctx => {
ctx.body = 'hello world';
});
router.get('/public/test', async ctx => {
ctx.body = 'hello public';
});
export { app, router };
b)测试用例
js
import request from 'supertest';
import koaSession from 'koa-session';
import { app, router } from './app';
import login from '../lib/index';
// 目标中间件的依赖
app.keys = ['test'];
app.use(koaSession({key: 'loginTest'}, app));
const onLogout = jest.fn();
// 需要测试的中间件
app.use(login.router({
anonymousPrefixList: ['/public'],
onLogout,
}));
app.use(router.routes());
// 发送请求的对象
const server = request(app.callback());
describe('login(默认配置)', () => {
it('退出登录', async () => {
const response = await server.get('/login/logout');
expect(response.status).toBe(302);
expect(response.header.location.includes('/site/logout.html')).toBeTruthy();
expect(onLogout).toHaveBeenCalledTimes(1);
});
});
4. 为固定内容断言
对不怎么需要变动、多个地方都需要断言的文案,可用 snapshot
,文案变动时用命令更新snapshot
即可。
比如上面router的响应正文,如果写字符串判断,响应内容变动,断言都需要改动。
js
// 以下两个用例,处理中设置用户session,请求可以到达controller,能成功响应
it('xx方式登录', async () => {
const response = await server.get('/')
.set('x-xx-nick', 'test');
expect(response.status).toBe(200);
// expect(response.text).toBe('hello world');
expect(response.text).toMatchSnapshot();
});
it('单测环境,自动登录', async () => {
process.env = { NODE_ENV: 'test' };
const response = await server.get('/')
.set('x-unittest-nick', 'test');
expect(response.status).toBe(200);
// expect(response.text).toBe('hello world');
expect(response.text).toMatchSnapshot();
// 重置环境变量
process.env = {};
});
更新文案
5. 为错误捕获断言
js
describe('parseEmail', function() {
it('收件人邮箱错误', async () => {
try{
const offStaffs = new Set();
await parseEmail('amyli;test@@xxxx.com', offStaffs);
}catch(error) {
expect(error.code).toBe(errorCode.PARAMS_VALIDATE);
}
});
});
测试通过,看上去这个用例写的没问题,但实际上,此用例不会失败。
将参数改成正确的邮箱:
js
it('收件人邮箱错误', async () => {
try{
const offStaffs = new Set();
await parseEmail('test@xxxx.com', offStaffs);
console.log('收件人邮箱正确');
}catch(error) {
expect(error.code).toBe(errorCode.PARAMS_VALIDATE);
}
});
可以看到,参数正确,无异常抛出,用例通过,但这个用例达不到测试错误参数的目的。
此类情况,可以在测试用例中增加要求------有断言被调用过:
js
it('收件人邮箱错误', async () => {
try{
const offStaffs = new Set();
// 或者 expect.assertions(1);
expect.hasAssertions();
await parseEmail('test@xxxx.com', offStaffs);
}catch(error) {
expect(error.code).toBe(errorCode.PARAMS_VALIDATE);
}
});