前端测试工具 Jest 的断言与模拟函数使用

你好,我是木亦。

作为现代前端工程中最受欢迎的测试工具之一 ,Jest 以"零配置"的特性俘获了众多开发者的心。但真正发挥 Jest 威力的两大核心技能------断言(Assertions)与模拟函数(Mock Functions) ,却让很多初学者望而却步。这篇文章将通过大量真实案例,带你看清这两个核心概念的底层逻辑,助你写出专业级测试代码!


一、万丈高楼平地起:Jest 断言体系全解析

1.1 断言与测试用例的血脉联系

断言就像质量检测员,每个断言都在验证程序的某个特定行为是否符合预期。当我们在测试文件中写上这样一段代码:

scss 复制代码
test('最简单的断言示例', () => {
  expect(1 + 1).toBe(2);
});

这里的expect().toBe()就是典型的 Jest 断言链,它构成了测试用例的验证核心

1.2 九大常用断言实战

我们根据实际使用频率和场景整理了最常用的断言清单表:

断言方法 适用场景 示例代码
.toBe() 基础值比较 expect(42).toBe(42)
.toEqual() 对象/数组深度比较 expect(obj).toEqual({a:1})
.toBeTruthy() 验证是否为真值 expect('text').toBeTruthy()
.toHaveLength() 验证数组/字符串长度 expect(arr).toHaveLength(3)
.toThrow() 验证抛出异常 expect(fn).toThrow()
.toContain() 验证包含元素 expect(['a','b']).toContain('a')
.toBeGreaterThan() 数字大小比较 expect(5).toBeGreaterThan(3)
.toMatch() 正则匹配 expect('abc').toMatch(/b/)
.resolves/.rejects 异步代码验证 await expect(promise).resolves.toBe(1)

值得注意的对比:

scss 复制代码
// 对象比较的陷阱案例
test('对象比较的坑', () => {
  const obj = { id: 1 };

  expect(obj).toBe({ id: 1 });    // ✖️ 失败,比较对象引用
  expect(obj).toEqual({ id: 1 }); // ✔️ 正确方法
});

1.3 深度解密异步测试

异步测试是前端场景中的重中之重,这里提供三种主流解决方案:

Promise 的优雅处理

scss 复制代码
test('获取用户数据', () => {
  return fetchUser().then(user => {
    expect(user.name).toBe('John');
  });
});

Async/Await 的现代风

scss 复制代码
test('新版异步写法', async () => {
  const user = await fetchUser();
  expect(user.id).toBeGreaterThan(0);
});

回调地狱的解药

ini 复制代码
test('传统回调测试', done => {
  fetchUser(user => {
    expect(user.age).toBe(30);
    done(); // 必须调用
  });
});

二、模拟的艺术:Mock 函数完全攻略

2.1 为什么要模拟函数?

真实的线上环境存在各种不确定因素:[图片上传失败]、[接口返回异常]、[第三方服务超时]...通过模拟我们可以:

  • ✅ 隔离外部依赖
  • ✅ 构造各种测试场景
  • ✅ 捕获函数调用参数
  • ✅ 测试边界条件

2.2 三种 Mock 场景实战

2.2.1 基础函数模拟

javascript 复制代码
// 创建模拟函数
const mockFn = jest.fn();

// 设置返回值为固定值
mockFn.mockReturnValue(42);
console.log(mockFn()); // 42

// 动态返回值
mockFn.mockImplementation((n) => n * 2);
console.log(mockFn(3)); // 6

// Promise模拟
mockFn.mockResolvedValue('success');
await mockFn().then(data => {
  console.log(data); // 'success'
});

2.2.2 模块方法劫持

当需要模拟第三方模块时非常有用:

javascript 复制代码
// userAPI.js
export const getUser = () => {
  // 真实网络请求...
};

// 测试文件
import { getUser } from './userAPI';

jest.mock('./userAPI', () => ({
  getUser: jest.fn().mockResolvedValue({
    name: 'Mock用户'
  })
}));

test('模块模拟测试', async () => {
  const user = await getUser();
  expect(user.name).toContain('Mock');
});

2.2.3 高阶函数追踪器

scss 复制代码
const mathUtils = {
  multiply: (a, b) => a * b,
};

test('函数调用追踪', () => {
  mathUtils.multiply = jest.fn();
  mathUtils.multiply(2, 3);

  expect(mathUtils.multiply)
    .toHaveBeenCalledWith(2, 3);  // ✔️验证调用参数

  expect(mathUtils.multiply.mock.calls.length)
    .toBe(1);  // 直接访问Mock属性
});

2.3 模拟函数的高级应用

模拟不同的连续返回值:

scss 复制代码
const mockRoll = jest.fn()
    .mockReturnValueOnce(1)
    .mockReturnValueOnce(2)
    .mockReturnValue(3);

// 测试结果
mockRoll(); // 1
mockRoll(); // 2
mockRoll(); // 3

复杂模块的部分模拟:

kotlin 复制代码
// 原模块功能保留,只模拟部分方法
jest.mock('axios', () => {
  const actual = jest.requireActual('axios');
  return {
    ...actual,
    get: jest.fn().mockResolvedValue({ data: 'mock' }),
  };
});

三、真实项目实践案例

3.1 表单校验函数测试

php 复制代码
// 表单验证函数
function validateForm(values) {
  const errors = {};
  if (!values.username) errors.username = '必填字段';
  if (values.age < 18) errors.age = '未满18岁';
  return errors;
}

// 测试用例
test('表单验证应返回错误信息', () => {
  expect(validateForm({}))
    .toEqual({
      username: '必填字段',
      age: '未满18岁'
    });

  expect(validateForm({ username: 'Tom', age: 20 }))
    .toEqual({});
});

3.2 用户登录流程测试

javascript 复制代码
// 测试用户登录流程
test('用户登录成功流程', async () => {
  // 模拟登录接口
  mockLoginAPI.mockResolvedValue({
    success: true,
    token: 'fake-token'
  });

  const result = await login('user', 'pass');

  expect(mockLoginAPI)
    .toHaveBeenCalledWith('user', 'pass');

  expect(localStorage.setItem)
    .toHaveBeenCalledWith('token', 'fake-token');
});

四、最佳实践与避坑指南

✅ 推荐做法

  1. 优先使用expect().toEqual()进行对象校验
  2. Mock命名用mock前缀提升可读性(如mockFetch
  3. 使用.toHaveBeenCalledTimes()验证调用次数
  4. 为每个测试案例独立beforeEach重置Mock

⚠️ 常见问题

  1. 对象引用问题 :始终用toEqual替代toBe进行对象校验
  2. 异步未等待 :遗漏async/await导致假通过
  3. Mock残留污染 :忘记在beforeEach中调用jest.clearAllMocks()
  4. 过度模拟:无需Mock的纯函数应该直接测试

五、向高阶进发:Jest 生态延伸

想要更上一层楼?这些扩展方向值得探索:

  1. 快照测试(Snapshot Testing) :可视化UI组件输出对比
  2. 覆盖率报告(Coverage Report) :通过--coverage参数生成
  3. 定时器模拟(Fake Timers) :优雅测试setTimeout等时间逻辑
  4. E2E测试整合:与Cypress/Puppeteer配合使用

测试驱动开发的力量

当单元测试覆盖率从0到100%时,你会见证代码质量的蜕变升华。在持续集成的时代,良好的测试习惯不仅能提升代码质量 ,更是一张亮眼的技术名片。记住:每一个高质量的测试用例,都是在为项目的稳健运行保驾护航!

相关推荐
@PHARAOH1 小时前
HOW - 在 Mac 上的 Chrome 浏览器中调试 Windows 场景下的前端页面
前端·chrome·macos
月月大王3 小时前
easyexcel导出动态写入标题和数据
java·服务器·前端
JC_You_Know4 小时前
多语言网站的 UX 陷阱与国际化实践陷阱清单
前端·ux
Python智慧行囊4 小时前
前端三大件---CSS
前端·css
Jinuss4 小时前
源码分析之Leaflet中Marker
前端·leaflet
成都渲染101云渲染66664 小时前
blender云渲染指南2025版
前端·javascript·网络·blender·maya
聆听+自律4 小时前
css实现渐变色圆角边框,背景色自定义
前端·javascript·css
牛马程序小猿猴5 小时前
17.thinkphp的分页功能
前端·数据库
huohuopro6 小时前
Vue3快速入门/Vue3基础速通
前端·javascript·vue.js·前端框架
草巾冒小子6 小时前
vue3中解决 return‘ inside ‘finally‘ block报错的问题
前端·javascript·vue.js