4、Jest 核心功能:匹配器、钩子函数与 Mock 模拟
好,咱们今天来啃 Jest 最核心的三个功能。说实话,这三个东西你玩明白了,前端自动化测试基本就入门了。我在团队里带新人时,发现很多人卡就卡在「不知道用什么断言」和「不知道怎么模拟依赖」上。今天咱们一次性说透。
4.1 匹配器:你的断言工具箱
匹配器(Matchers)说白了就是 Jest 提供的一堆断言方法。你想想看,测试的本质是什么?就是「我期望这个值是某个结果」。匹配器就是帮你表达这个「期望」的工具。
4.1.1 toBe 与 toEqual:最常用的两个
toBe 用的是 Object.is(),比较的是引用。而 toEqual 会递归检查对象的每个字段。我刚开始写测试时犯过一个低级错误:用 toBe 比较两个结构相同的对象,结果测试一直挂。后来才反应过来——引用不同啊!
test('toBe 与 toEqual 的区别', () => {
const obj1 = { name: 'Jest' };
const obj2 = { name: 'Jest' };
// toBe 比较引用,这里会失败
// expect(obj1).toBe(obj2); // ❌
// toEqual 比较值,这里通过
expect(obj1).toEqual(obj2); // ✅
// 基本类型用 toBe 没问题
expect(2 + 2).toBe(4);
});
核心原则:基本类型用 toBe,对象/数组用 toEqual。记住这条,能省掉你 80% 的调试时间。
4.1.2 toContain:数组与字符串的利器
这个匹配器我几乎每天都会用。检查数组里有没有某个元素,或者字符串里有没有某个子串,一行代码搞定。
test('toContain 的使用场景', () => {
const shoppingList = ['牛奶', '面包', '鸡蛋'];
expect(shoppingList).toContain('牛奶');
expect(shoppingList).not.toContain('可乐');
const message = 'Hello Jest World';
expect(message).toContain('Jest');
});
小技巧:如果你要检查数组里是否包含某个对象,用 toContainEqual。它会做深度比较,而不是引用比较。
4.1.3 其他常用匹配器一览
| 匹配器 | 用途 | 示例 |
|---|---|---|
toBeNull() |
只匹配 null | expect(x).toBeNull() |
toBeUndefined() |
只匹配 undefined | expect(x).toBeUndefined() |
toBeTruthy() |
匹配所有真值 | expect(1).toBeTruthy() |
toBeFalsy() |
匹配所有假值 | expect(0).toBeFalsy() |
toMatch() |
正则匹配字符串 | expect('abc').toMatch(/bc/) |
toThrow() |
检查函数是否抛出异常 | expect(fn).toThrow() |
注意:toBeTruthy 和 toBe(true) 不是一回事。前者只要值能转为 true 就通过,后者必须是严格的 true。我曾经在项目里用混了,导致一个本该失败的测试通过了,排查了半天才发现是匹配器用错了。
4.2 钩子函数:测试的生命周期管理
钩子函数解决的是「重复代码」的问题。你想想看,如果每个测试用例都要创建同一个对象、连接同一个数据库,那代码得多冗余?
4.2.1 beforeEach 与 afterEach
这两个是最常用的。每个测试用例执行前后都会跑一次。我个人习惯把「数据准备」放在 beforeEach 里,把「清理工作」放在 afterEach 里。
let data;
beforeEach(() => {
// 每个测试前都重新初始化
data = { count: 0, items: [] };
console.log('数据已重置');
});
afterEach(() => {
// 每个测试后清理
data = null;
console.log('数据已清理');
});
test('数据初始状态', () => {
expect(data.count).toBe(0);
});
test('添加数据', () => {
data.items.push('新项目');
expect(data.items).toHaveLength(1);
});
关键点:每个测试用例之间应该是隔离的。beforeEach 保证了每个用例拿到的都是「干净」的数据。我曾经见过一个项目,因为没在 beforeEach 里重置状态,导致测试用例之间互相影响,跑出来的结果时对时错,特别坑。
4.2.2 beforeAll 与 afterAll
这两个只执行一次。适合做「一次性」的初始化,比如连接数据库、读取配置文件。但要注意——如果某个测试用例修改了共享数据,后面的用例可能会受影响。
let dbConnection;
beforeAll(() => {
// 只执行一次,比如建立数据库连接
dbConnection = { connected: true };
console.log('数据库已连接');
});
afterAll(() => {
// 所有测试结束后关闭连接
dbConnection = null;
console.log('数据库已断开');
});
test('数据库连接状态', () => {
expect(dbConnection.connected).toBe(true);
});
避坑指南:我曾经在 beforeAll 里初始化了一个全局计数器,结果多个测试用例都在修改它,最后测试结果完全不可预测。记住:beforeAll 适合「只读」的初始化,如果每个用例需要独立的数据,用 beforeEach。
4.2.3 钩子的作用域
钩子可以放在 describe 块内部,只对该块内的测试生效。这个特性在组织大型测试文件时特别有用。
describe('用户模块', () => {
let user;
beforeEach(() => {
user = { name: '张三', role: 'admin' };
});
test('用户有名字', () => {
expect(user.name).toBe('张三');
});
describe('权限检查', () => {
beforeEach(() => {
// 这个钩子只对当前 describe 内的测试生效
user.permissions = ['read', 'write'];
});
test('管理员有读写权限', () => {
expect(user.permissions).toContain('write');
});
});
});
4.3 Mock 函数与模块模拟
Mock 是测试里最强大的功能之一。说白了,就是「我不想真的调用某个函数,但我需要知道它有没有被调用、被怎么调用的」。我在项目中遇到过很多次,测试一个模块时它依赖了第三方 API,总不能每次跑测试都真的去请求网络吧?这时候 Mock 就派上用场了。
4.3.1 创建 Mock 函数
用 jest.fn() 可以创建一个 Mock 函数。它能记录自己被调用的次数、参数、返回值等信息。
test('Mock 函数的基本用法', () => {
const mockFn = jest.fn();
mockFn('hello', 'world');
mockFn('jest');
// 检查调用次数
expect(mockFn).toHaveBeenCalledTimes(2);
// 检查某次调用的参数
expect(mockFn).toHaveBeenCalledWith('hello', 'world');
expect(mockFn).toHaveBeenLastCalledWith('jest');
});
4.3.2 模拟返回值
你可以让 Mock 函数返回你指定的值。这个在测试「依赖外部服务」的代码时特别有用。
test('模拟返回值', () => {
const mockFn = jest.fn()
.mockReturnValueOnce('第一次调用')
.mockReturnValueOnce('第二次调用')
.mockReturnValue('默认返回值');
expect(mockFn()).toBe('第一次调用');
expect(mockFn()).toBe('第二次调用');
expect(mockFn()).toBe('默认返回值');
expect(mockFn()).toBe('默认返回值');
});
实用技巧:用 mockReturnValueOnce 可以模拟「第一次调用返回 A,第二次返回 B」的场景。我在测试一个重试逻辑时就用到了这个,模拟前两次失败、第三次成功,完美验证了重试机制。
4.3.3 模块模拟
有时候你需要模拟整个模块。比如你的代码里用了 axios 发请求,测试时你肯定不想真的发出去。用 jest.mock() 可以自动模拟整个模块。
// 假设有一个 api.js 文件
// export const fetchData = () => axios.get('/api/data');
// 在测试文件中
jest.mock('./api');
import { fetchData } from './api';
test('模拟整个模块', async () => {
// 设置模拟返回值
fetchData.mockResolvedValue({ data: '模拟数据' });
const result = await fetchData();
expect(result.data).toBe('模拟数据');
expect(fetchData).toHaveBeenCalledTimes(1);
});
核心思想:模块模拟让你能「隔离」被测试的代码。你只测试当前模块的逻辑,不关心它的依赖到底怎么实现。这就像修车时,你只检查发动机,不用管轮胎是不是原厂的。
4.3.4 模拟时间
这个功能我特别喜欢。测试里如果有 setTimeout 或 setInterval,真的等时间过去太慢了。用 jest.useFakeTimers() 可以模拟时间流逝。
jest.useFakeTimers();
test('模拟定时器', () => {
const callback = jest.fn();
setTimeout(callback, 1000);
// 时间还没到,callback 没被调用
expect(callback).not.toHaveBeenCalled();
// 快进时间
jest.advanceTimersByTime(1000);
// 现在 callback 被调用了
expect(callback).toHaveBeenCalledTimes(1);
});
注意:用了 jest.useFakeTimers() 后,记得在 afterEach 里调用 jest.useRealTimers() 恢复真实时间。不然其他测试可能会受影响。我刚开始用的时候忘了恢复,结果其他测试里的定时器全都不走了,排查了好久才发现是这个问题。
4.4 小结
这三个核心功能,你掌握了就能写出一手漂亮的测试用例。匹配器帮你准确断言结果,钩子函数帮你管理测试生命周期,Mock 让你能隔离外部依赖。嗯,说白了,这就是 Jest 测试的「三板斧」。下一章咱们会把这些知识串起来,写一个完整的实战案例。