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

前端 潘老师 2个月前 (03-02) 29 ℃ (0) 扫码查看

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


版权声明:本站文章,如无说明,均为本站原创,转载请注明文章来源。如有侵权,请联系博主删除。
本文链接:https://www.panziye.com/front/15223.html
喜欢 (0)
请潘老师喝杯Coffee吧!】
分享 (0)
用户头像
发表我的评论
取消评论
表情 贴图 签到 代码

Hi,您需要填写昵称和邮箱!

  • 昵称【必填】
  • 邮箱【必填】
  • 网址【可选】