今天给大家讲讲前端测试工具Jest里关键的两个部分——断言和模拟函数。很多小伙伴在接触Jest的时候,对这俩都有点摸不着头脑,别担心,这篇文章会通过超多实际例子,带大家搞懂它们的底层逻辑,教大家如何使用Jest 前端测试工具中断言与模拟函数。

一、深入了解Jest断言体系

(一)断言和测试用例的紧密关系

断言在测试里就像是一个严格的质量检查员,它的任务就是检查程序的每一个行为是不是和我们预先设想的一样。咱们看下面这段测试代码:

test('最简单的断言示例', () => { expect(1 + 1).toBe(2); }); 

这里面的expect().toBe()就是Jest里很典型的断言组合,它可是测试用例里验证结果的核心部分。就好比你做了一道数学题,得检查答案对不对,这个断言就是在帮你做这个检查。

(二)九大常用断言实战操作

根据大家平时使用的频率和不同的测试场景,我给大家整理了一份超实用的常用断言清单。

  • .toBe():基础值比较:用来判断两个基础值是否相等,像expect(42).toBe(42),就是看看前面的值是不是和后面的一样。
  • .toEqual():对象/数组深度比较:当需要比较两个对象或者数组里的每一个元素是不是都一样时,就用它。比如expect(obj).toEqual({a:1})
  • .toBeTruthy():验证是否为真值:可以用来检查某个值是不是“真的”,像字符串'text',它不是空的,所以expect('text').toBeTruthy() 就会通过。
  • .toHaveLength():验证数组/字符串长度:想知道数组里有几个元素,或者字符串有多长,就用这个断言。例如expect(arr).toHaveLength(3) ,就是检查数组arr的长度是不是3。
  • .toThrow():验证抛出异常:有些函数在特定情况下应该报错,这时候就用它。比如expect(fn).toThrow() ,看看函数fn调用的时候会不会抛出异常。
  • .toContain():验证包含元素:检查数组或者字符串里有没有某个元素。像expect(['a','b']).toContain('a') ,就是看看数组里有没有'a'这个元素。
  • .toBeGreaterThan():数字大小比较:比较两个数字大小,expect(5).toBeGreaterThan(3) ,就是判断5是不是大于3。
  • .toMatch():正则匹配:当需要检查字符串是不是符合某个规则时,就用正则表达式来匹配。比如expect('abc').toMatch(/b/) ,就是看看字符串'abc'里有没有包含字母b
  • .resolves/.rejects:异步代码验证:在处理异步操作的时候,用这个来检查异步操作是成功还是失败。例如await expect(promise).resolves.toBe(1)

这里有个很重要的对比要注意:

// 对象比较的陷阱案例 test('对象比较的坑', () => { const obj = { id: 1 }; expect(obj).toBe({ id: 1 }); // ✖️ 失败,比较对象引用 expect(obj).toEqual({ id: 1 }); // ✔️ 正确方法 }); 

在比较对象的时候,toBe是比较对象的引用地址,两个看起来一样的对象,地址可能不同,所以一般用toEqual来比较对象里的内容。

(三)深度解析异步测试

在前端开发里,异步操作特别常见,下面给大家分享三种主流的异步测试方法。

  • Promise的优雅处理
test('获取用户数据', () => { return fetchUser().then(user => { expect(user.name).toBe('John'); }); }); 

这里用fetchUser()这个异步函数获取用户数据,然后通过then来处理获取到的数据,再用断言检查数据是不是符合预期。

  • Async/Await的现代风
test('新版异步写法', async () => { const user = await fetchUser(); expect(user.id).toBeGreaterThan(0); }); 

async/await让异步代码看起来更像同步代码,用await等待异步操作完成,拿到数据后再进行断言验证。

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

这种传统的回调方式也能进行异步测试,不过要记得在测试完成后调用done() ,不然测试可能不会结束。

二、Mock函数全方位解析

(一)为什么要用Mock函数

在真实的线上环境里,会有各种各样的不确定因素,像图片上传失败、接口返回异常、第三方服务超时等等。这时候,Mock函数就派上用场啦!它可以帮我们隔离外部依赖,模拟出各种测试场景,还能捕获函数调用时的参数,测试一些边界情况。

(二)三种Mock场景实际操作

  • 基础函数模拟
// 创建模拟函数 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' }); 

先创建一个模拟函数mockFn,然后可以给它设置固定的返回值,也能让它根据传入的参数动态返回值,还能模拟成Promise形式。

  • 模块方法劫持:在需要模拟第三方模块的时候,这个方法特别有用。
// 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'); }); 

这里模拟了getUser这个函数,让它返回我们预设的数据,方便在测试的时候使用。

  • 高阶函数追踪器
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属性 }); 

通过把mathUtils.multiply变成模拟函数,我们可以追踪它有没有被调用,以及调用时的参数是什么。

(三)模拟函数的高级应用

  • 模拟不同的连续返回值
const mockRoll = jest.fn() .mockReturnValueOnce(1) .mockReturnValueOnce(2) .mockReturnValue(3); // 测试结果 mockRoll(); // 1 mockRoll(); // 2 mockRoll(); // 3 

这样设置后,每次调用mockRoll函数,返回的值都不一样,方便测试一些依赖多次调用返回不同结果的场景。

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

对于像axios这样比较复杂的模块,我们可以只模拟其中的部分方法,其他方法还是保留原来的功能。

三、真实项目中的实践案例

(一)表单校验函数测试

// 表单验证函数 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({}); }); 

这个表单验证函数用来检查表单里的数据合不合格,通过测试用例可以验证它在不同情况下返回的结果是不是正确。

(二)用户登录流程测试

// 测试用户登录流程 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'); }); 

这里模拟了登录接口的返回数据,然后检查登录函数有没有正确调用接口,以及登录成功后有没有把token存到本地存储里。

四、最佳实践和避坑小提示

(一)推荐做法

在对象校验的时候,优先用expect().toEqual() ;给Mock函数命名的时候,加上mock前缀,像mockFetch,这样别人一看就知道是Mock函数;用.toHaveBeenCalledTimes() 来验证函数被调用的次数;每个测试案例最好用beforeEach来重置Mock,保证测试之间不会互相影响。

(二)常见问题

比较对象的时候,一定不要用toBe,要用toEqual,不然很可能因为对象引用的问题导致测试出错;写异步测试的时候,别忘了async/await ,不然可能会出现假通过的情况;每次测试完,记得在beforeEach里调用jest.clearAllMocks() ,不然Mock的结果可能会残留,影响下一次测试;对于那些没有外部依赖的纯函数,直接测试就行,不用Mock。

五、探索Jest生态进阶玩法

如果大家还想更深入地学习Jest,下面这些方向可以去探索一下。

  • 快照测试(Snapshot Testing):可以把UI组件的输出结果拍个“快照”,下次测试的时候对比一下,看看有没有变化,就像给组件拍照片,方便检查有没有改坏。
  • 覆盖率报告(Coverage Report):通过--coverage这个参数,能生成代码的覆盖率报告,看看哪些代码被测试覆盖到了,哪些还没有。
  • 定时器模拟(Fake Timers):在测试涉及到setTimeout这些时间逻辑的代码时,用它来模拟时间,让测试更准确。
  • E2E测试整合:可以把Jest和Cypress、Puppeteer这些工具一起用,实现更全面的测试。

在开发过程中,单元测试覆盖率从0提升到100%的时候,你会发现代码质量有质的飞跃!在现在持续集成的大环境下,养成良好的测试习惯不仅能让代码更靠谱,还是你技术实力的体现呢!大家一定要重视起来,每个高质量的测试用例都是项目稳定运行的保障!