你好,我是木亦。
作为现代前端工程中最受欢迎的测试工具之一 ,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');
});
四、最佳实践与避坑指南
✅ 推荐做法
- 优先使用
expect().toEqual()
进行对象校验 - Mock命名用
mock
前缀提升可读性(如mockFetch
) - 使用
.toHaveBeenCalledTimes()
验证调用次数 - 为每个测试案例独立
beforeEach
重置Mock
⚠️ 常见问题
- 对象引用问题 :始终用
toEqual
替代toBe
进行对象校验 - 异步未等待 :遗漏
async/await
导致假通过 - Mock残留污染 :忘记在
beforeEach
中调用jest.clearAllMocks()
- 过度模拟:无需Mock的纯函数应该直接测试
五、向高阶进发:Jest 生态延伸
想要更上一层楼?这些扩展方向值得探索:
- 快照测试(Snapshot Testing) :可视化UI组件输出对比
- 覆盖率报告(Coverage Report) :通过
--coverage
参数生成 - 定时器模拟(Fake Timers) :优雅测试setTimeout等时间逻辑
- E2E测试整合:与Cypress/Puppeteer配合使用
测试驱动开发的力量
当单元测试覆盖率从0到100%时,你会见证代码质量的蜕变升华。在持续集成的时代,良好的测试习惯不仅能提升代码质量 ,更是一张亮眼的技术名片。记住:每一个高质量的测试用例,都是在为项目的稳健运行保驾护航!