如何使用 Jest 前端测试工具中断言与模拟函数
今天给大家讲讲前端测试工具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%的时候,你会发现代码质量有质的飞跃!在现在持续集成的大环境下,养成良好的测试习惯不仅能让代码更靠谱,还是你技术实力的体现呢!大家一定要重视起来,每个高质量的测试用例都是项目稳定运行的保障!