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

82 阅读4分钟

你好,我是木亦。

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


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

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

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

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)

值得注意的对比:

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

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

1.3 深度解密异步测试

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

Promise 的优雅处理

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

Async/Await 的现代风

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

回调地狱的解药

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

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

2.1 为什么要模拟函数?

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

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

2.2 三种 Mock 场景实战

2.2.1 基础函数模拟

// 创建模拟函数
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 模块方法劫持

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

// 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 高阶函数追踪器

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 模拟函数的高级应用

模拟不同的连续返回值:

const mockRoll = jest.fn()
    .mockReturnValueOnce(1)
    .mockReturnValueOnce(2)
    .mockReturnValue(3);

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

复杂模块的部分模拟:

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

三、真实项目实践案例

3.1 表单校验函数测试

// 表单验证函数
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 用户登录流程测试

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

掘金.png