Back

JavaScript 中的单元测试与集成测试:何时使用哪种测试

JavaScript 中的单元测试与集成测试:何时使用哪种测试

每个 JavaScript 开发者在构建可靠应用时都会面临同样的问题:应该编写更多单元测试还是集成测试?选择错误,你要么花费数小时调试脆弱的测试,要么将 bug 发布到生产环境。答案不在于二选一——而在于理解每种类型何时能提供最大价值。

本文将阐明 JavaScript 中单元测试和集成测试的区别,为你展示每种测试的实际示例,并提供一个决策框架来平衡测试策略中的两者。

核心要点

  • 单元测试验证隔离的代码片段,追求速度和精确性
  • 集成测试验证组件交互,提供真实场景的信心
  • 70-20-10 的分布(单元测试-集成测试-端到端测试)适用于大多数 JavaScript 项目
  • 根据测试对象选择:算法需要单元测试,工作流需要集成测试

什么是单元测试?

单元测试验证独立的代码片段是否在隔离状态下正常工作。可以把它想象成在将乐高积木添加到结构之前测试单个积木。

在 JavaScript 中,单元测试通常关注:

  • 单个函数或方法
  • 不包含子组件的单个组件
  • 特定的类或模块

单元测试示例

// calculator.js
export function calculateDiscount(price, percentage) {
  if (percentage < 0 || percentage > 100) {
    throw new Error('Invalid percentage');
  }
  return price * (1 - percentage / 100);
}

// calculator.test.js
import { calculateDiscount } from './calculator';

test('applies 20% discount correctly', () => {
  expect(calculateDiscount(100, 20)).toBe(80);
});

test('throws error for invalid percentage', () => {
  expect(() => calculateDiscount(100, 150)).toThrow('Invalid percentage');
});

单元测试擅长于:

  • 速度:每个测试只需毫秒级时间,因为没有 I/O 或外部依赖
  • 精确性:精准定位故障位置
  • 稳定性:很少因无关更改而失败

什么是集成测试?

集成测试验证应用程序的多个部分是否能正确协同工作。不是单独测试乐高积木,而是测试多个积木如何连接。

JavaScript 中的集成测试通常涵盖:

  • 组件与 API 的交互
  • 多个模块协同工作
  • 数据库操作与业务逻辑
  • UI 组件与状态管理

集成测试示例

// userProfile.test.js
import { render, screen, waitFor } from '@testing-library/react';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import UserProfile from './UserProfile';

const server = setupServer(
  rest.get('/api/user/:id', (req, res, ctx) => {
    return res(ctx.json({ 
      id: req.params.id, 
      name: 'Jane Doe',
      role: 'Developer' 
    }));
  })
);

beforeAll(() => server.listen());
afterAll(() => server.close());

test('displays user data after loading', async () => {
  render(<UserProfile userId="123" />);
  
  expect(screen.getByText(/loading/i)).toBeInTheDocument();
  
  await waitFor(() => {
    expect(screen.getByText('Jane Doe')).toBeInTheDocument();
    expect(screen.getByText('Developer')).toBeInTheDocument();
  });
});

集成测试提供:

  • 信心:验证真实的用户工作流
  • 覆盖率:测试组件之间的交互
  • 真实性:捕获单元测试遗漏的问题

重要的关键差异

范围和隔离性

单元测试使用 mock 和 stub 隔离代码。你控制每个变量。 集成测试尽可能使用真实实现,只在外部边界(如 API 或数据库)处进行 mock。

执行速度

单元测试每个运行 1-50 毫秒。你可以在几秒内运行数千个测试。 集成测试需要 100-500 毫秒或更长时间。它们涉及设置、清理,有时还有真实的 I/O 操作。

维护成本

单元测试仅在其特定单元更改时才会失败。 集成测试可能因测试流程中任何位置的更改而失败,需要更多调查时间。

Bug 检测

单元测试捕获隔离代码中的逻辑错误和边界情况。 集成测试捕获连接问题、错误假设和组件之间的契约违规。

何时使用每种类型

编写单元测试用于:

  • 纯函数:业务逻辑、计算、数据转换
  • 复杂算法:排序、搜索、验证规则
  • 边界情况:错误处理、边界条件
  • 工具函数:格式化器、解析器、辅助函数

编写集成测试用于:

  • API 交互:HTTP 请求、响应处理
  • 用户工作流:多步骤流程、表单提交
  • 组件集成:父子组件通信
  • 状态管理:Redux actions、Context API 流程
  • 数据库操作:带业务逻辑的 CRUD 操作

实用测试策略

大多数成功的 JavaScript 项目遵循 70-20-10 的分布:

  • 70% 单元测试:快速反馈,易于调试
  • 20% 集成测试:对组件交互的信心
  • 10% 端到端测试:关键路径的最终验证

这不是硬性规则——根据应用类型进行调整。API 密集型应用需要更多集成测试。算法密集型库需要更多单元测试。

CI/CD 流水线集成

构建流水线以获得快速反馈:

  1. 提交前:运行单元测试(< 5 秒)
  2. Pull Request:运行所有单元测试和集成测试
  3. 部署前:运行包括端到端测试在内的完整测试套件

使用 Jest 进行单元测试,Testing Library 进行集成测试,以及 MSW 进行 API mock,可以使这个流水线高效且易于维护。

需要避免的常见陷阱

  1. 集成测试中过度 mock:违背了测试交互的目的
  2. 测试实现细节:关注行为,而非内部结构
  3. 忽视测试速度:缓慢的测试会阻碍频繁运行
  4. 在出现 bug 后才编写测试:主动测试可以预防问题

结论

JavaScript 中的单元测试和集成测试不是相互竞争的策略——它们是互补的工具。单元测试为隔离逻辑提供速度和精确性。集成测试提供组件正确协同工作的信心。

从业务逻辑和纯函数的单元测试开始。为关键用户路径和组件交互添加集成测试。跳过关于测试哲学的宗教式辩论,专注于能让你有信心发布代码的方法。

最好的 JavaScript 测试策略是在保持高开发速度的同时,能在用户发现之前捕获 bug 的策略。根据应用需求平衡两种类型,并根据最常出现问题的地方进行调整。

常见问题

在单元测试中始终 mock 外部依赖。这包括数据库、API、文件系统和其他服务。Mock 确保测试运行快速、保持可预测性,并真正测试你的代码隔离状态,而不是外部系统的行为。

首先问问什么可能会出错。如果逻辑本身很复杂,先编写单元测试。如果功能涉及多个组件相互通信或外部服务,优先考虑集成测试。大多数功能都能从两种类型中受益。

单元测试应该在 10 秒内完成整个套件。集成测试可以需要 1-5 分钟。如果测试时间更长,将它们拆分为并行任务或识别需要优化的慢速测试。快速的测试鼓励开发者频繁运行它们。

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay