集成测试验证你自己的组件和模块能否协同工作——它们在 CI 中以毫秒级速度运行,并对外部系统进行模拟;端到端测试则像真实用户一样通过真实 UI 驱动整个运行中的应用,针对已部署或预发布构建运行,不模拟任何内容。这一核心区别解决了大多数混淆,但它隐藏了前端工作中特有的第二个问题:JavaScript 开发者眼中的”集成”与后端 QA 工程师眼中的”集成”含义不同,而且浏览器中”集成”与”E2E”之间的界限本就模糊。
本文将在 Web 技术栈的语境下划定这条界限。文章提供了清晰的定义和具体示例,从真正影响决策的维度进行对比,说明两者相对于单元测试的位置,介绍与之对应的 JavaScript 工具(Vitest、Jest、Testing Library、MSW、Playwright、Cypress),并给出如何分配测试预算的明确建议——而非”两者都用”的模糊之词。
核心要点
- 集成测试在网络被模拟的情况下将你自己的模块组合渲染,并在 CI 中以毫秒级速度运行;端到端测试在真实浏览器中驱动已部署的应用,不模拟任何内容。
- 在 JavaScript 应用中,“集成”通常意味着 Vitest 或 Jest 配合 Testing Library 和 MSW;“端到端”通常意味着 Playwright 或 Cypress 针对真实 URL 运行。
- 前端的集成/E2E 边界是一个连续谱,而非一堵墙——你在哪里划线是预算决策,而非硬性规定。
- 以集成测试为主以获得最佳的置信度/成本比,在最高价值的用户旅程上保留三到五个 E2E 测试的小型套件,不要尝试对所有内容都做 E2E 测试。
- 生产环境中的 bug,从定义上讲,就是你的测试从未断言过的用户旅程,因此对真实故障的会话回放是找到缺失测试的最快途径。
集成测试与端到端测试概览
这两种测试类型在所有影响预算决策的维度上都有所不同:覆盖范围、运行速度、运行位置、模拟内容、误报频率,以及各自能独家捕获的 bug 类型。
| 评判标准 | 集成测试 | 端到端(E2E)测试 |
|---|---|---|
| 范围 | 你自己的若干模块组合(例如组件 + 数据层) | 整个运行中的应用,从前端到后端 |
| 速度 | 毫秒到数秒 | 每个测试数秒到数分钟 |
| 运行位置 | 本地和 CI 中,在 Node 或 jsdom 环境 | 针对已部署或预发布构建,在真实浏览器中 |
| 模拟内容 | 外部系统——网络、第三方 API | 不模拟(或尽可能少模拟) |
| 不稳定性 | 低——无真实网络,无真实浏览器时序 | 较高——依赖整个系统保持稳定 |
| 成本/维护 | 编写和保持绿色的成本较低 | 较高;随着应用变化,套件容易腐化 |
| 独家捕获 | 损坏的 API 契约、格式错误的响应、状态连接问题 | 认证、重定向、环境配置、第三方脚本、跨页面状态 |
集成测试是什么,以 Web 为例
Discover how at OpenReplay.com.
集成测试将你自己的一组组件组合渲染,在网络被桩代码替换的情况下断言其组合行为,验证那些已经过单元测试的单元是否真正协同工作。它不启动浏览器,也不访问真实服务器。
一个具体示例:一个 LoginForm 组件连接到认证 hook 和 profile 数据获取逻辑。集成测试渲染表单,填写字段,提交,并断言欢迎状态出现——但 POST /api/login 和 GET /api/profile 请求被拦截并由 mock 响应。你测试的是表单、hook、请求层与渲染结果之间的连接。你不是在测试真实后端、认证提供商或 CDN。
契约类 bug 就在这一层浮现:组件期望 user.name,但处理器返回 user.fullName;401 路径从不重新渲染错误提示;或者加载标志永远不清除。集成测试能捕获存在于模块之间的 bug——损坏的 API 契约、格式错误的响应、状态连接问题——而无需真实部署的成本。
端到端测试是什么,以 Web 为例
端到端测试在真实浏览器中驱动你实际部署的应用,完全模拟真实用户的操作,断言用户所见的内容——不模拟任何东西。它会经历真实服务器、真实数据库、真实认证流程、真实重定向,以及页面加载的任何第三方脚本。
典型示例是用户登录并完成结账:导航到线上 URL,输入凭据,提交,进入仪表板,将商品加入购物车,完成支付,确认订单。每一层都参与其中——前端打包产物、网络、API、数据库、Session Cookie、支付集成。
E2E 测试能捕获只在真实部署中存在的 bug:认证和重定向、环境配置、第三方脚本、内容安全策略(CSP)违规,以及在页面导航后仍然存在的跨页面状态。这些问题在网络被模拟时无法复现,这正是 E2E 对于那些产生收入的用户旅程值得其较高成本的原因。
两者的位置:测试金字塔与测试奖杯
这两种测试类型都位于单元测试之上——单元测试用于隔离地检查单个函数或组件。经典的测试金字塔规定:底部是大量单元测试,中间是较少的集成测试,顶部是极少的慢速 E2E 测试。这是保持测试套件快速运行的合理默认方案。
现代的对应方案是测试奖杯,由 Kent C. Dodds 推广,它加厚了集成层,因为集成测试为 Web 应用提供了最佳的置信度/成本比——接近真实使用场景,但没有完整 E2E 的不稳定性和运行时间。其理论依据来自 Testing Library 的核心原则:你的测试越接近软件的使用方式,它们能给你的信心就越大。一个渲染真实组件树并断言用户可见行为的集成测试,比测试孤立 reducer 的单元测试更接近真实使用场景——而且只需毫秒级时间。将测试奖杯视为 Web 应用中被广泛采纳的现代观点,而非对金字塔的普遍替代。
模糊的前端边界
在前端工作中,集成/E2E 边界是一个连续谱,而非一堵墙:渲染组件子树的 Testing Library 测试是”集成”,加载真实 URL 的 Playwright 测试是”E2E”,而你在哪里划线是预算决策,而非硬性规定。这条模糊边界是混淆的最大根源,因为同一个词在不同场合含义不同。
后端 QA 文献将集成测试分为自底向上、自顶向下和大爆炸策略——这套分类法是真实存在的,但它是服务器集成的行话,对使用 Vitest 和 Playwright 的人来说几乎没有帮助。忽略它。对 Web 开发者而言,实际的映射关系才是关键:“集成”几乎总是意味着在网络被模拟的情况下渲染组件树,而”E2E”几乎总是意味着在真实浏览器中驱动已部署的应用。中间的灰色地带——比如一个 Playwright 测试指向本地开发服务器,同时用 MSW 拦截网络——完全可行;只需有意识地决定它归属于预算的哪一侧。
各自对应的 JS 工具链及代码示例
在 JavaScript 应用中,“集成测试”通常意味着渲染组件树并在网络被模拟的情况下断言其行为——通常使用 Vitest 或 Jest 配合 Testing Library 和 MSW——而”端到端测试”意味着使用 Playwright 或 Cypress 在真实浏览器中驱动已部署的应用。
Mock Service Worker(MSW)是让集成测试在没有真实后端的情况下依然真实可信的关键。它在两种环境中都在网络层拦截请求——在浏览器中通过 Service Worker API(setupWorker),在 Node 中通过底层请求拦截(setupServer,供 Vitest 或 Jest 测试使用,无需 service worker 文件)。由于拦截发生在边界处,而非通过 patch fetch 实现,相同的处理器可以同时用于集成测试、开发服务器,甚至 Playwright 运行;官方文档直接指出:“想象一下,在开发、集成测试、端到端测试以及 Storybook 中使用相同的 API mock。”
以下是同一个登录并获取 profile 功能的集成测试示例。注意 MSW v2 的 http + HttpResponse API,它替代了 v1 的 res/ctx 模式:
// login.integration.test.tsx — Vitest 4 + @testing-library/react 16 + MSW 2
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
import { LoginForm } from './LoginForm'
const server = setupServer(
http.post('/api/login', () =>
HttpResponse.json({ token: 'abc' })
),
http.get('/api/profile', () =>
HttpResponse.json({ name: 'Ada' })
)
)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
test('logs in and shows the profile name', async () => {
render(<LoginForm />)
await userEvent.type(screen.getByLabelText(/email/i), 'ada@example.com')
await userEvent.type(screen.getByLabelText(/password/i), 'hunter2')
await userEvent.click(screen.getByRole('button', { name: /sign in/i }))
expect(await screen.findByText(/welcome, ada/i)).toBeInTheDocument()
})
不启动浏览器,不运行服务器,网络完全受控——因此这个测试快速且具有确定性。同一功能的 E2E 测试则针对已部署的应用运行,不模拟任何内容。Playwright 通过一套 API 驱动 Chromium、Firefox 和 WebKit,具备自动等待和以 Web 为优先的断言机制:
// login.e2e.spec.ts — Playwright 1.6x
import { test, expect } from '@playwright/test'
test('user logs in and sees their profile', async ({ page }) => {
await page.goto('/login')
await page.getByLabel('Email').fill('ada@example.com')
await page.getByLabel('Password').fill('hunter2')
await page.getByRole('button', { name: 'Sign in' }).click()
await expect(
page.getByRole('heading', { name: /welcome, ada/i })
).toBeVisible()
})
断言看起来相似;不同的是底层的一切。Playwright 规范验证了真实的认证端点、重定向和 Session Cookie 在真实浏览器中正常工作——并为此付出了运行时间和不稳定性的代价。有一个值得了解的实际限制:由于异步 React Server Components 是新特性,Jest 目前不支持它们,Next.js 建议对异步组件使用 E2E 测试——这是一个集成层暂时无法覆盖、E2E 因此物有所值的具体案例。
按场景选择的决策规则
根据你试图预防的 bug 类型来选择测试类型,而非凭习惯。规则如下:
- 新模块交互或前后端契约 → 集成测试。将组件连接到新端点、将新 reducer 连接到新 selector、将表单连接到验证层:渲染组件树,用 MSW 模拟网络,断言行为。
- 跨整个技术栈的关键用户旅程 → 一个 E2E 测试。登录、注册、结账,那个一旦损坏就会造成收入或信任损失的流程。
- 其他所有情况 → 倾向于集成测试,不要对其做 E2E 测试。
对 Web 团队的实际建议:以集成测试为主以获得最佳置信度/成本比,在最高价值的用户旅程上保留三到五个端到端测试的小型套件,不要尝试对所有内容都做 E2E 测试。对 tooltip 做 E2E 测试是维护负担;对其做集成测试则是低成本的保险。
为什么 E2E 套件会腐化,以及如何保持稳定
端到端套件会腐化,因为每个测试都依赖整个系统保持不变;解决方案是保持测试数量少、断言用户可见的结果而非实现细节,并删除那些不再保护收入的测试。一个重命名的测试 ID、较慢的预发布部署、不稳定的第三方组件,或一个改变的重定向,都可能在没有真实回归的情况下让通过的套件变红——而一个频繁误报的套件会被忽视。具体而言:
- 优先使用基于角色和标签的选择器,而非脆弱的 CSS 路径;
- 依靠 Playwright 的自动等待,而非固定的 sleep;
- 隔离测试数据;
- 控制测试数量,使套件保持足够快速以值得信赖。
然而,没有任何套件能覆盖你没有想到的用户旅程——生产环境中的 bug,从定义上讲,就是你的集成和 E2E 测试从未断言过的路径。会话回放正是在这里发挥价值:对真实故障的回放能展示导致问题的确切未测试旅程——故障发生时的 DOM 状态、网络请求和控制台错误。登录和结账流程的会话回放经常揭示本地测试从未触发的故障模式,例如特定环境的重定向或被 CSP 阻止的第三方脚本。专业的做法是将该复现转化为缺失的测试:旅程级别的故障变成一个新的 Playwright 规范,组件或契约故障变成一个新的 Testing Library + MSW 集成测试,同样的 bug 就不会再次回归。
结论
在生产环境中经得住考验的分配方式是:以集成测试为主,少量高价值 E2E 测试,底层是针对纯逻辑的单元测试——集成测试因为每秒运行时间能买到最多的置信度,E2E 测试因为某些 bug 只存在于真实部署的浏览器中。从现在开始,用 Vitest 或 Jest、Testing Library 和 MSW 为下一个功能的模块边界编写集成测试,将 Playwright 留给少数你承担不起损坏的用户旅程,让真实的生产故障告诉你忘记写了哪个测试。
常见问题
Playwright 或 Cypress 这样的单一工具可以同时编写集成测试和端到端测试吗?
可以,但区别在于你如何界定测试的范围,而非工具本身。Playwright 和 Cypress 都可以在隔离环境中挂载组件并模拟网络,这实际上是集成测试;或者加载已部署的 URL 并测试整个技术栈,这就是端到端测试。两者都有组件测试模式,但在 JavaScript 生态系统中,Vitest 或 Jest 配合 Testing Library 仍然是更常见的集成测试路径。
在 Vitest 或 Jest 集成测试中使用 MSW 需要 mockServiceWorker.js 文件吗?
不需要。mockServiceWorker.js 文件只在使用 setupWorker 的浏览器路径中需要,该路径依赖 Service Worker API。基于 Node 的集成测试通过 setupServer 运行,它使用底层类扩展来拦截请求,而非 service worker,因此不需要生成任何文件。只有在真实浏览器中模拟请求时——例如本地开发或基于浏览器的测试运行——才需要生成 worker 脚本。
什么时候应该写端到端测试而不是集成测试?
当你试图预防的 bug 只存在于真实部署中时,才编写端到端测试——认证、重定向、环境配置、Session Cookie、第三方脚本、内容安全策略违规,或跨页面状态。这些问题在网络被模拟时无法复现。对于存在于你自己模块之间的所有问题,例如损坏的 API 契约、格式错误的响应或状态连接问题,集成测试更快、更便宜、更稳定。将端到端测试留给你最高价值的收入旅程。
为什么我的端到端测试在本地通过,但在 CI 中失败?
端到端套件依赖整个系统保持不变,因此本地和 CI 环境之间的差异会在没有真实回归的情况下导致测试失败。常见原因包括:使用固定 sleep 而非自动等待、在构建之间发生变化的脆弱 CSS 选择器、CI 部署时序较慢、共享或未隔离的测试数据,以及不稳定的第三方组件。优先使用基于角色和标签的选择器,依靠 Playwright 的自动等待,每次运行时隔离测试数据,并保持套件足够小以维持可靠性。