JS 单元测试 (`Jest` / `Mocha`) 高级:快照测试、Mocking 与 Stubbing

嘿,大家好!欢迎来到今天的单元测试高级技巧分享会,我是你们的老朋友,今天咱们聊聊快照测试、Mocking 和 Stubbing 这些测试里的“高级玩家”。别担心,虽然名字听起来高大上,但实际上掌握了它们,你的单元测试就能更上一层楼,bug 也无处遁形。

一、热身:为什么要玩高级的?

在开始之前,咱们先简单回顾一下,为什么要折腾这些高级技巧。

  • 更全面的覆盖: 基础的单元测试可能只关注函数的输入输出,但高级技巧能让你深入到组件的内部状态、副作用等等,覆盖更广。
  • 更精准的定位: 当测试失败时,高级技巧能提供更详细的信息,帮你快速找到问题的根源。
  • 更灵活的测试: 某些情况下,直接测试依赖项会很困难,高级技巧能让你模拟这些依赖项,从而隔离被测代码。

二、快照测试:给你的 UI 拍张“身份证照”

快照测试,顾名思义,就是给你的组件拍一张“照片”,然后保存下来。每次运行测试时,都会将当前组件渲染的结果与之前保存的“照片”进行对比。如果不一样,就说明组件可能发生了意外的改变。

1. 适用场景:

  • UI 组件的渲染结果
  • 配置文件的内容
  • 任何可以通过序列化成字符串的数据结构

2. 代码示例 (Jest):

假设我们有一个简单的 React 组件:

// MyComponent.jsx
import React from 'react';

function MyComponent({ name }) {
  return (
    <div>
      <h1>Hello, {name}!</h1>
      <p>This is a snapshot test example.</p>
    </div>
  );
}

export default MyComponent;

现在,我们来编写一个快照测试:

// MyComponent.test.js
import React from 'react';
import { render } from '@testing-library/react';
import MyComponent from './MyComponent';

it('renders correctly', () => {
  const { asFragment } = render(<MyComponent name="World" />);
  expect(asFragment()).toMatchSnapshot();
});

解释:

  • render(<MyComponent name="World" />): 渲染我们的组件。
  • asFragment(): 将组件渲染成一个 DocumentFragment,方便进行快照比较。
  • toMatchSnapshot(): 这是 Jest 提供的快照测试核心方法,它会将当前的组件渲染结果与之前保存的快照进行比较。如果快照不存在,则会自动生成一个。

第一次运行测试:

Jest 会自动生成一个 __snapshots__ 文件夹,里面包含一个 MyComponent.test.js.snap 文件,这就是我们的快照文件。它的内容可能类似这样:

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders correctly 1`] = `
<DocumentFragment>
  <div>
    <h1>
      Hello, World!
    </h1>
    <p>
      This is a snapshot test example.
    </p>
  </div>
</DocumentFragment>
`;

修改组件并再次运行测试:

如果我们修改了 MyComponent.jsx 的内容,例如将 <h1> 标签的文本改为 "Greetings, World!",再次运行测试时,Jest 会发现快照不匹配,并报错。

如何更新快照:

当你确认组件的修改是正确的,并且你希望更新快照时,可以使用以下命令:

jest -u

或者

jest --updateSnapshot

这会将新的组件渲染结果保存为新的快照。

3. 注意事项:

  • 不要提交大量的快照更改: 快照更改应该谨慎对待,确保每次更改都是有意的。
  • 忽略动态内容: 如果你的组件包含动态内容(例如时间戳),你应该在快照测试中忽略它们。可以使用 Jest 的 expect.any(String) 或自定义的匹配器来实现。
  • 定期审查快照: 定期审查快照,确保它们仍然反映了组件的预期行为。

三、Mocking:模拟你的“替身演员”

Mocking 是一种创建“替身演员”来替代真实依赖项的技术。这允许你隔离被测代码,并控制依赖项的行为。

1. 适用场景:

  • 外部 API 调用
  • 数据库访问
  • 文件系统操作
  • 任何难以直接测试的依赖项

2. 代码示例 (Jest):

假设我们有一个函数,它会调用一个外部 API 来获取用户信息:

// userService.js
import axios from 'axios';

async function getUser(userId) {
  const response = await axios.get(`/users/${userId}`);
  return response.data;
}

export default getUser;

现在,我们来编写一个单元测试,使用 Mocking 来模拟 axios 的行为:

// userService.test.js
import getUser from './userService';
import axios from 'axios';

jest.mock('axios'); // 告诉 Jest 模拟 axios 模块

it('fetches user data successfully', async () => {
  const mockUserData = { id: 1, name: 'John Doe' };
  axios.get.mockResolvedValue({ data: mockUserData }); // 设置 axios.get 的 mock 实现

  const user = await getUser(1);

  expect(user).toEqual(mockUserData);
  expect(axios.get).toHaveBeenCalledTimes(1); // 验证 axios.get 是否被调用
  expect(axios.get).toHaveBeenCalledWith('/users/1'); // 验证 axios.get 的参数是否正确
});

it('handles errors when fetching user data', async () => {
  axios.get.mockRejectedValue(new Error('Network error')); // 设置 axios.get 的 mock 实现,模拟错误

  await expect(getUser(1)).rejects.toThrow('Network error');
});

解释:

  • jest.mock('axios'): 告诉 Jest 模拟 axios 模块。这会将 axios 替换为一个 Mock 对象,你可以控制它的行为。
  • axios.get.mockResolvedValue({ data: mockUserData }): 设置 axios.get 的 mock 实现。当 axios.get 被调用时,它会返回一个 Promise,该 Promise resolve 的值为 { data: mockUserData }
  • axios.get.mockRejectedValue(new Error('Network error')): 设置 axios.get 的 mock 实现,模拟错误。当 axios.get 被调用时,它会返回一个 Promise,该 Promise reject 的值为 new Error('Network error')
  • expect(axios.get).toHaveBeenCalledTimes(1): 验证 axios.get 是否被调用了一次。
  • expect(axios.get).toHaveBeenCalledWith('/users/1'): 验证 axios.get 的参数是否为 '/users/1'
  • expect(getUser(1)).rejects.toThrow('Network error'): 验证 getUser(1) 是否会抛出 Network error 错误。

3. Mocking 的方式:

  • jest.mock(moduleName, factory): 模拟整个模块。factory 是一个可选的函数,用于自定义 Mock 对象的实现。
  • jest.spyOn(object, methodName): 监视对象的方法。这允许你验证方法是否被调用,以及它的参数。但不会改变方法的原始实现,除非你手动覆盖它。
  • jest.fn(implementation): 创建一个 Mock 函数。你可以完全控制 Mock 函数的行为。

3. 注意事项:

  • 不要过度 Mock: 只 Mock 那些难以直接测试的依赖项。
  • 确保 Mock 的行为与真实依赖项一致: 如果 Mock 的行为与真实依赖项不一致,你的测试结果可能会误导你。
  • 在测试完成后清理 Mock: 可以使用 jest.clearAllMocks()jest.resetAllMocks()jest.restoreAllMocks() 来清理 Mock,避免影响其他测试。

四、Stubbing:制造“替身”,但只控制返回值

Stubbing 类似于 Mocking,但它更侧重于控制依赖项的返回值,而不关心依赖项的内部实现。

1. 适用场景:

  • 只需要控制依赖项的返回值,而不需要验证它的调用情况。
  • 需要模拟依赖项的不同返回值,以便测试被测代码的不同分支。

2. 代码示例 (Jest):

假设我们有一个函数,它会根据配置文件的值来决定是否启用某个功能:

// featureService.js
import config from './config';

function isFeatureEnabled(featureName) {
  return config.features[featureName];
}

export default isFeatureEnabled;

现在,我们来编写一个单元测试,使用 Stubbing 来控制 config.features 的值:

// featureService.test.js
import isFeatureEnabled from './featureService';
import * as config from './config'; // 导入 config 模块

it('returns true if the feature is enabled', () => {
  const configSpy = jest.spyOn(config, 'features', 'get'); // 监视 config.features 属性的 getter
  configSpy.mockReturnValue({ featureA: true }); // 设置 config.features 的 mock 返回值

  expect(isFeatureEnabled('featureA')).toBe(true);

  configSpy.mockRestore(); // 恢复 config.features 的原始实现
});

it('returns false if the feature is disabled', () => {
    const configSpy = jest.spyOn(config, 'features', 'get'); // 监视 config.features 属性的 getter
    configSpy.mockReturnValue({ featureA: false }); // 设置 config.features 的 mock 返回值

    expect(isFeatureEnabled('featureA')).toBe(false);

    configSpy.mockRestore(); // 恢复 config.features 的原始实现
});

解释:

  • jest.spyOn(config, 'features', 'get'): 监视 config.features 属性的 getter。'get' 参数告诉 spyOn 我们要监视 getter。
  • configSpy.mockReturnValue({ featureA: true }): 设置 config.features 的 mock 返回值。当访问 config.features 属性时,它会返回 { featureA: true }
  • configSpy.mockRestore(): 恢复 config.features 的原始实现。这非常重要,可以避免影响其他测试。

3. Stubbing 的方式:

  • 使用 jest.spyOn 监视对象的方法或属性,然后使用 mockReturnValuemockResolvedValuemockRejectedValue 来控制返回值。
  • 直接修改对象的属性值(不推荐,因为可能会影响其他测试)。

4. 注意事项:

  • 确保在测试完成后恢复原始实现: 可以使用 mockRestore()restoreAllMocks() 来恢复原始实现。
  • 不要过度 Stubbing: 只 Stub 那些需要控制返回值的依赖项。

五、Mocking vs. Stubbing:傻傻分不清楚?

Mocking 和 Stubbing 经常被混淆,它们之间的区别主要在于:

特性 Mocking Stubbing
关注点 验证依赖项的调用情况(例如调用次数、参数) 控制依赖项的返回值
主要用途 隔离被测代码,并验证依赖项的行为 隔离被测代码,并模拟依赖项的不同返回值
常用方法 jest.mock, jest.spyOn, toHaveBeenCalledTimes, toHaveBeenCalledWith jest.spyOn (配合 mockReturnValue, mockResolvedValue, mockRejectedValue)
是否验证行为

简单来说:

  • Mocking: 我不仅要控制你的行为,还要检查你是否按照我的预期去做了。
  • Stubbing: 我只关心你的返回值,其他的我不在乎。

六、总结:磨刀不误砍柴工

快照测试、Mocking 和 Stubbing 都是强大的单元测试工具,它们能帮助你编写更全面、更精准、更灵活的测试。但是,它们也需要谨慎使用,避免过度使用或使用不当。

记住,单元测试的目的是为了提高代码质量,而不是为了追求测试覆盖率。只有真正理解了这些高级技巧,才能将它们运用到实际项目中,发挥它们最大的价值。

好了,今天的分享就到这里,希望大家有所收获。如果还有什么疑问,欢迎随时提问!下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注