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

相关推荐
拾光拾趣录10 分钟前
for..in 和 Object.keys 的区别:从“遍历对象属性的坑”说起
前端·javascript
OpenTiny社区21 分钟前
把 SearchBox 塞进项目,搜索转化率怒涨 400%?
前端·vue.js·github
编程猪猪侠1 小时前
Tailwind CSS 自定义工具类与主题配置指南
前端·css
qhd吴飞1 小时前
mybatis 差异更新法
java·前端·mybatis
YGY Webgis糕手之路1 小时前
OpenLayers 快速入门(九)Extent 介绍
前端·经验分享·笔记·vue·web
患得患失9491 小时前
【前端】【vueDevTools】使用 vueDevTools 插件并修改默认打开编辑器
前端·编辑器
ReturnTrue8681 小时前
Vue路由状态持久化方案,优雅实现记住表单历史搜索记录!
前端·vue.js
UncleKyrie1 小时前
一个浏览器插件帮你查看Figma设计稿代码图片和转码
前端
遂心_1 小时前
深入解析前后端分离中的 /api 设计:从路由到代理的完整指南
前端·javascript·api
你听得到112 小时前
Flutter - 手搓一个日历组件,集成单日选择、日期范围选择、国际化、农历和节气显示
前端·flutter·架构