Jest 最佳实践

FreeGuideOnline 最新 2026-07-15

src/ utils/ formatDate.js tests/ formatDate.test.js components/ Button.jsx tests/ Button.test.jsx


- **优点**:测试与源码紧密关联,容易定位。
- **适用场景**:组件或工具函数的单元测试。

#### 1.2 独立 `tests/` 目录与镜像结构

为整个项目创建一个顶层的 `tests/` 目录,并复制 `src/` 的目录结构。

src/ utils/formatDate.js components/Button.jsx tests/ utils/formatDate.test.js components/Button.test.jsx


- **优点**:清晰分离生产代码与测试代码,构建时可轻松排除测试。
- **适用场景**:大型项目或希望测试与源码物理分离的团队。

#### 1.3 命名约定

- 测试文件以 `.test.js` 或 `.spec.js` 结尾。
- 描述性名称:`userService.fetchUser.test.js` 优于 `user.test.js`。
- 对同一模块的多种测试场景,可使用 `.test` 复数或子目录。

#### 1.4 避免单个测试文件过大

当某文件的测试超过 200 行时,考虑按功能或行为拆分成多个测试文件。例如,对 `apiClient` 可按 HTTP 方法或业务域拆分:

api/ tests/ apiClient.get.test.js apiClient.post.test.js apiClient.auth.test.js


### 二、编写可读性高的测试描述

测试即文档,良好的命名让测试意图一目了然。

#### 2.1 使用 `describe` 进行分组

用嵌套 `describe` 表达测试的上下文层次。

```javascript
describe('UserService', () => {
  describe('getUserById', () => {
    describe('when user exists', () => {
      it('should return the user object', () => { /* ... */ });
    });
    describe('when user does not exist', () => {
      it('should throw NotFoundError', () => { /* ... */ });
    });
  });
});

2.2 it 语句的语义化描述

遵循 “should ... when ...” 模式,使描述具有行为驱动开发 (BDD) 风格。

  • 正面:'should return the formatted date when input is valid'
  • 负面:'should throw an error when passing null'
  • 边缘:'should return null when input is undefined'

避免技术实现细节,侧重行为。

2.3 避免测试代码中的逻辑

测试中不应出现条件语句、循环或复杂计算。每个测试只验证一种行为,保持代码线性、无分支。

// ❌ 不推荐
it('processes array', () => {
  const results = process(input);
  results.forEach((item, index) => {
    if (index === 0) expect(item).toBe(1);
    else expect(item).toBeGreaterThan(1);
  });
});

// ✅ 推荐
it('should return 1 as the first element', () => {
  const results = process(input);
  expect(results[0]).toBe(1);
});

it('should return numbers greater than 1 for other elements', () => {
  const results = process(input);
  for (let i = 1; i < results.length; i++) {
    expect(results[i]).toBeGreaterThan(1);
  }
});

三、断言策略

选择正确的断言匹配器,让测试输出提供清晰的失败信息。

3.1 精确匹配 vs 灵活匹配

  • 基本数据类型:使用 .toBe() 进行严格相等。
  • 对象/数组:使用 .toEqual() 进行深度比较。
  • 部分匹配:对对象子集使用 .toMatchObject()
  • 包含检查:对数组或字符串使用 .toContain().toContainEqual()
  • 异常:使用 .toThrow() 并检查错误类或消息。
// 错误对象匹配
await expect(asyncFunction()).rejects.toThrow(ValidationError);
await expect(asyncFunction()).rejects.toThrow('Invalid email');

// 深层部分匹配
expect(response).toMatchObject({
  status: 200,
  data: { user: { id: expect.any(Number) } }
});

3.2 使用对称匹配器提高可读性

利用 Jest 内建的非对称匹配器 expect.any(constructor)expect.stringContaining() 等,让测试意图更清晰。

expect(saveMock).toHaveBeenCalledWith({
  id: expect.any(String),
  createdAt: expect.any(Date),
  name: 'Alice'
});

3.3 避免过度的快照测试

快照测试适合验证序列化输出不变的场景(如组件渲染结果、配置文件)。但对于复杂的动态数据,快照会变得庞大且难以审查。

  • 使用小粒度快照,只针对确定性部分。
  • 对业务逻辑用断言代替快照。
  • 定期修剪和更新快照,确保文件包含有效快照。

四、模拟 (Mock) 最佳实践

模拟外部依赖是单元测试的核心。正确地使用 Mock 可以隔离被测单元并加速测试。

4.1 优先使用 jest.fn()jest.spyOn()

  • jest.fn() 创建独立 mock 函数。
  • jest.spyOn(object, method) 用于监视现有方法并可选地模拟实现。
const mockCallback = jest.fn();
someAsyncTask(mockCallback);
expect(mockCallback).toHaveBeenCalledWith('success');

4.2 每次测试后清理 Mock

afterEach 中重置 mock 状态,避免测试间相互影响。

afterEach(() => {
  jest.clearAllMocks(); // 清空调用记录和实例
  // 或 jest.resetAllMocks()  同时清除实现和返回值
  // 或 jest.restoreAllMocks() 恢复原始实现(针对 spyOn)
});

4.3 避免全局 Mock 污染

尽量在 setupFiles 或单个测试套件中 Mock 模块,而不是在全项目范围 mock 核心库(如 fsaxios),除非有统一配置的需求。局部 mock 更易于理解和维护。

// 在测试文件顶部 mock 模块
jest.mock('../weatherService');

对于多次使用的 mock,可以抽成 __mocks__ 目录下的手动 mock 文件,便于共享。

4.4 模拟时保持实现的一致性

自定义 mock 实现应当遵循真实 API 的行为,否则测试可能给出虚假的信心。

// ✅ 返回合理的数据形状
jest.mock('../api', () => ({
  fetchUser: jest.fn().mockResolvedValue({ id: 1, name: 'Test' })
}));

// ❌ 返回不符合约定的值
fetchUser.mockResolvedValue('not an object');

4.5 对时间依赖的函数使用假计时器

jest.useFakeTimers() 控制 setTimeoutDate 等,避免测试不稳定和真实等待。

beforeEach(() => {
  jest.useFakeTimers();
  jest.setSystemTime(new Date('2023-01-01'));
});

afterEach(() => {
  jest.useRealTimers();
});

it('should expire token after 1 hour', () => {
  const token = createToken();
  jest.advanceTimersByTime(3600_000);
  expect(isTokenValid(token)).toBe(false);
});

五、异步测试

正确处理异步操作是 Jest 测试中的常见陷阱。

5.1 使用 async/await.resolves / .rejects

it('resolves with user data', async () => {
  await expect(fetchUser(1)).resolves.toEqual({ id: 1, name: 'Alice' });
});

it('rejects with error message', async () => {
  await expect(fetchUser(999)).rejects.toThrow('User not found');
});

5.2 确保断言被调用

忘记 await 或返回 Promise 会导致测试提前结束而漏报错误。

// ❌ 错误:测试立即通过,但实际断言可能未执行
it('does not work', () => {
  fetchUser(1).then(data => {
    expect(data.id).toBe(1);
  });
});

// ✅ 正确:返回 promise
it('works', () => {
  return fetchUser(1).then(data => {
    expect(data.id).toBe(1);
  });
});

5.3 测试事件循环中的微任务

jest.runAllTimers() 适用于宏任务(setTimeout),await 通常已覆盖微任务。对于复杂的 Promise 链,直接 await 目标函数即可。

六、测试覆盖率与质量

覆盖率是衡量测试完备性的一个指标,但不是唯一目标。

6.1 设定合理阈值

jest.config.js 中设置覆盖率阈值,但避免盲目追求 100%。

coverageThreshold: {
  global: {
    branches: 80,
    functions: 80,
    lines: 80,
    statements: 80
  }
}

6.2 关注核心逻辑的覆盖率

优先保证业务逻辑、数据转换、权限控制等的测试覆盖。对于一些纯样板代码(如接口定义、简单 getter/setter)可以适当放宽。

6.3 审查未覆盖的行

使用 jest --coverage 生成 HTML 报告,定期检查未覆盖的代码分支,判断是缺少测试还是死代码。

七、配置与性能优化

Jest 的默认配置已经非常友好,但大型项目中可以微调。

7.1 利用 --findRelatedTests

只运行与变更文件相关的测试,提升开发时反馈速度。

jest --findRelatedTests src/utils/formatDate.js

7.2 谨慎使用 --maxWorkers

在 CI 环境中,限制 worker 数量以避免内存过载。通常设为 250%

jest --maxWorkers=2