jest 单元测试

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);
    }
});
相关推荐
非著名架构师几秒前
js混淆的方式方法
开发语言·javascript·ecmascript
努力的布布15 分钟前
SpringMVC源码-AbstractHandlerMethodMapping处理器映射器将@Controller修饰类方法存储到处理器映射器
java·后端·spring
PacosonSWJTU20 分钟前
spring揭秘25-springmvc03-其他组件(文件上传+拦截器+处理器适配器+异常统一处理)
java·后端·springmvc
记得开心一点嘛29 分钟前
在Java项目中如何使用Scala实现尾递归优化来解决爆栈问题
开发语言·后端·scala
多多米100534 分钟前
初学Vue(2)
前端·javascript·vue.js
敏编程1 小时前
网页前端开发之Javascript入门篇(5/9):函数
开发语言·javascript
黄俊懿1 小时前
【深入理解SpringCloud微服务】手写实现各种限流算法——固定时间窗、滑动时间窗、令牌桶算法、漏桶算法
java·后端·算法·spring cloud·微服务·架构
看到请催我学习1 小时前
内存缓存和硬盘缓存
开发语言·前端·javascript·vue.js·缓存·ecmascript
2401_857439691 小时前
“衣依”服装销售平台:Spring Boot技术应用与优化
spring boot·后端·mfc
Jerry.ZZZ2 小时前
系统设计,如何设计一个秒杀功能
后端