前端测试工具 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%时,你会见证代码质量的蜕变升华。在持续集成的时代,良好的测试习惯不仅能提升代码质量 ,更是一张亮眼的技术名片。记住:每一个高质量的测试用例,都是在为项目的稳健运行保驾护航!

相关推荐
斯~内克6 小时前
Electron 菜单系统深度解析:从基础到高级实践
前端·javascript·electron
数据知道6 小时前
【YAML】一文掌握 YAML 的详细用法(YAML 备忘速查)
前端·yaml
dr李四维6 小时前
vue生命周期、钩子以及跨域问题简介
前端·javascript·vue.js·websocket·跨域问题·vue生命周期·钩子函数
旭久6 小时前
react+antd中做一个外部按钮新增 表格内部本地新增一条数据并且支持编辑删除(无难度上手)
前端·javascript·react.js
windyrain6 小时前
ant design pro 模版简化工具
前端·react.js·ant design
浪遏6 小时前
我的远程实习(六) | 一个demo讲清Auth.js国外平台登录鉴权👈|nextjs
前端·面试·next.js
GISer_Jing7 小时前
React-Markdown详解
前端·react.js·前端框架
太阳花ˉ7 小时前
React(九)React Hooks
前端·react.js
拉不动的猪8 小时前
vue与react的简单问答
前端·javascript·面试