集成测试实战:从踩坑到高效验证的完整指南

上官玉惠 移动 阅读 2,680
赞 54 收藏
二维码
手机扫码查看
反馈

为什么我非要折腾集成测试方案?

说实话,我一开始对集成测试是有点抵触的。单元测试写得飞起,E2E 用 Cypress 跑个冒烟也行,但“集成测试”这四个字总让我觉得模糊又鸡肋。直到上个项目,一个看似简单的登录流程,因为 mock 太过理想化,上线后才发现和真实后端交互时 token 过期逻辑完全没覆盖,用户直接卡在空白页。那一刻我才意识到:单元测得太细,E2E 又太重,中间缺的正是能模拟真实接口调用、又不依赖完整 UI 渲染的那一环——也就是集成测试。

集成测试实战:从踩坑到高效验证的完整指南

于是最近半年,我认真试了三种主流方案:Jest + MSW(Mock Service Worker)、Cypress Component Testing(带网络拦截),还有 Vitest + 自定义 mock 模块。今天就来聊聊我的真实体验,不讲大道理,只说踩过的坑和省下的时间。

谁更灵活?谁更省事?

先说结论:我比较喜欢用 Jest + MSW。不是因为它最强大,而是它最“刚刚好”——既能拦截真实 fetch 请求,又能用接近生产环境的方式写测试,还不用启动整个浏览器。

举个例子,我们有个获取用户信息的接口,需要测试 token 过期时的重定向逻辑:

// api/user.js
export const fetchUser = async () => {
  const res = await fetch('/api/user');
  if (res.status === 401) {
    window.location.href = '/login';
    return null;
  }
  return res.json();
};

用 MSW 的话,mock 服务端行为非常直观:

// test/user.test.js
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { fetchUser } from '../api/user';

const server = setupServer(
  rest.get('/api/user', (req, res, ctx) => {
    return res(ctx.status(401));
  })
);

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

test('should redirect to login when 401', () => {
  // 模拟 location 跳转
  delete global.window.location;
  global.window.location = { href: '' };

  fetchUser();

  expect(global.window.location.href).toBe('/login');
});

这段代码亲测有效,而且不需要启动任何 UI 框架,纯 Node 环境就能跑。MSW 的优势在于它用 Service Worker 的思路在 Node 和浏览器里统一了 mock 逻辑,写一次,前后端都能用。我特别喜欢它用 ctx.status()ctx.json() 这种声明式方式控制响应,比手写 mock 函数清晰多了。

Cypress Component Testing:重,但看得见

当然,如果你项目里已经重度依赖 Cypress,那它的 Component Testing 模式也值得考虑。尤其是当你需要验证“组件在特定 API 响应下是否渲染正确”时,它能直接把组件挂到 DOM 上,所见即所得。

同样的场景,用 Cypress 写大概是这样:

// cypress/component/UserProfile.cy.jsx
import UserProfile from './UserProfile';
import { fetchUser } from '../../api/user';

cy.intercept('GET', '/api/user', {
  statusCode: 401,
  body: {}
}).as('getUser');

cy.mount(<UserProfile />);
cy.wait('@getUser');

// 验证是否跳转 —— 但这里有个坑!
cy.url().should('include', '/login');

看起来挺美,但实际用起来有几个问题:第一,Cypress 的 cy.interceptfetch 支持没问题,但如果你用了 axios 或自定义封装,得确保底层还是走原生 fetch/XHR;第二,跳转验证在 component test 里其实很别扭,因为 Cypress 默认不会真的跳转页面,你得额外配置 experimentalSingleTabRunMode 或者用 window.location.replace 监听,折腾了半天发现不如直接跑 E2E 测试。

所以我的结论是:Cypress Component Testing 适合 UI 渲染逻辑复杂的集成测试,比如“加载中状态 → 成功 → 失败”的完整状态流,但涉及路由跳转、全局副作用的场景,反而成了负担。

Vitest + 手动 mock:轻量,但容易翻车

有些团队追求极致速度,会选 Vitest + 手动 mock 模块。比如这样:

// test/user.test.js
import { vi } from 'vitest';
import { fetchUser } from '../api/user';

vi.mock('../api/user', () => {
  return {
    fetchUser: vi.fn().mockResolvedValue(null)
  };
});

// 或者更暴力地 mock 全局 fetch
global.fetch = vi.fn().mockResolvedValue({
  status: 401,
  json: async () => ({})
});

这种方式跑得飞快,Vitest 本身启动也快。但问题在于:mock 太“假”了。你模拟的是函数返回值,而不是真实的 HTTP 层。一旦你的业务逻辑里有 res.statusres.headers 判断,或者用了 AbortControllertimeout 等 fetch 特性,手动 mock 很容易漏掉细节,导致测试通过但线上崩了。

我之前就踩过一个坑:本地 mock 返回 { data: user },但实际接口返回的是 { user: { ... } },因为结构不同,解构时报错,但测试完全没发现问题。从那以后,我就尽量避免这种“过度简化”的 mock。

我的选型逻辑

现在我做新项目,集成测试基本按这个逻辑选:

  • 如果测试重点是“API 响应对业务逻辑的影响”(比如错误码处理、重试机制、缓存策略),我首选 Jest + MSW。它模拟的是网络层,不是函数层,更贴近真实。
  • 如果测试重点是“组件在各种数据下的 UI 表现”,比如表格加载、错误提示样式、骨架屏切换,我会用 Cypress Component Testing,配合 cy.intercept 拦截接口,眼见为实。
  • Vitest + 手动 mock 我基本不用了,除非是纯工具函数的集成(比如一个依赖多个 util 的函数),但那其实更接近单元测试。

另外,这里注意我踩过好几次坑:MSW 在 Node 环境下默认不会处理相对路径(比如 /api/user),需要配 setupServeronUnhandledRequest 选项,否则容易静默失败。建议加上:

setupServer({
  onUnhandledRequest: 'error' // 未 mock 的请求直接报错,避免漏测
});

最后一点现实主义

其实没有完美的方案。MSW 也有缺点:比如在测试中修改 handler 逻辑后,有时候需要 server.resetHandlers() 才能生效,否则会复用上一个测试的 mock;Cypress 虽然重,但它的 time travel 调试确实香。而 Vitest 快是快,但“快”不能替代“准”。

我现在的做法是:核心业务流程(登录、支付、提交)用 Jest + MSW 写集成测试,覆盖所有边界情况;UI 交互复杂的功能用 Cypress Component Test 补充;其他地方,能单元测试就单元测试,实在不行就靠 E2E 兜底。

以上是我个人对集成测试方案的完整对比和选型思路,有更优的实现方式欢迎评论区交流。毕竟,能稳稳上线的测试,才是好测试。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论