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);
    }
});
相关推荐
uzong2 小时前
技术故障复盘模版
后端
GetcharZp2 小时前
基于 Dify + 通义千问的多模态大模型 搭建发票识别 Agent
后端·llm·agent
桦说编程3 小时前
Java 中如何创建不可变类型
java·后端·函数式编程
IT毕设实战小研3 小时前
基于Spring Boot 4s店车辆管理系统 租车管理系统 停车位管理系统 智慧车辆管理系统
java·开发语言·spring boot·后端·spring·毕业设计·课程设计
wyiyiyi3 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
gnip4 小时前
vite和webpack打包结构控制
前端·javascript
阿华的代码王国4 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
Jimmy4 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
AntBlack5 小时前
不当韭菜V1.1 :增强能力 ,辅助构建自己的交易规则
后端·python·pyqt
烛阴5 小时前
前端必会:如何创建一个可随时取消的定时器
前端·javascript·typescript