- 测试
- 开发工具
- 前端
【免费下载链接】msw
The industry standard for API mocking in JavaScript.
test/目录是 Mock Service Worker(MSW)的集成测试主战场,它覆盖了库对外暴露的每一个执行域:HTTP、GraphQL、WebSocket、Server-Sent Events(SSE)以及库自身的 API(msw-api)。本文以 test/README.md 为骨架,结合仓库中实际的测试工具链(test/setup/)、Vitest 多项目配置(vitest.config.ts)与真实测试用例,系统讲解这套集成测试体系的分层逻辑、文件命名规范、defineTestNetwork工具的工作方式,以及如何在此基础上新增一个高质量的集成测试。读完本文,你将掌握 MSW 仓库中"一个测试文件如何同时在 Node 与浏览器两套运行时下运行"的核心机制。
一、测试目录的定位与领域划分
test/README.md开篇即明确了该目录的定位:存放 Mock Service Worker 的集成测试,并按"库的执行或 API 领域"(execution or API domain)对所有测试套件进行分类:
| 目录 | 覆盖的领域 |
|---|---|
test/protocols/http | HTTP mocking 测试 |
test/protocols/graphql | GraphQL mocking 测试 |
test/protocols/websocket | WebSocket mocking 测试 |
test/protocols/sse | Server-Sent Events mocking 测试 |
test/msw-api | 库的公开 API 测试 |
这与单元测试(src/**/*.test.ts,运行于unit项目)形成明确分工:集成测试聚焦"协议层是否在真实网络栈中被正确拦截和模拟",而不是单个工具函数的内部实现。例如test/protocols/http/basic.test.ts验证的是"对一个https://example.com请求,MSW 是否真的返回了 mock 响应"。
二、文件命名规范:四种后缀决定运行环境
test/README.md给出了整仓最重要的命名契约,这也是理解 MSW 测试架构的钥匙:
*.test.ts—— 在Node.js与Vitest Browser Mode两套运行时中同时运行;*.node.test.ts——只在 Node.js 中运行;*.browser.test.ts——只在 Vitest Browser Mode 中运行;*.pw.test.ts—— 使用Playwright驱动真实浏览器,用于整页生命周期或多标签页行为(如 Service Worker 注册、跨标签页状态同步、stop后移除监听器等)。
这套后缀契约在 vitest.config.ts 中得到了逐条落实。该配置通过projects定义了 7 个测试项目,并用groupOrder控制串行执行顺序:
unit(groupOrder: 0):只跑src/**/*.test.ts,即源码旁的单元测试;node(groupOrder: 1):环境为node,include: ['test/**/*.test.ts'],同时显式exclude掉**/*.browser.test.ts、**/*.memory.test.ts和**/*.pw.test.ts——这就保证了*.node.test.ts与*.test.ts中的 Node 场景只在 Node 环境执行;memory(groupOrder: 2):内存泄漏专项测试(*.memory.test.ts),使用pool: 'forks'并注入--expose-gc,测试超时放宽到 120 秒;memory-browser(groupOrder: 3):通过浏览器命令(见 test/setup/browser-commands.ts)检查页面堆内存,用于度量 worker 网络的泄漏;browser(groupOrder: 4):启用 Vitest Browser Mode,include: ['test/**/*.test.ts']但exclude掉*.node.test.ts与*.memory.test.ts——与node项目互补,实现"同名测试文件在 Node 和浏览器中各跑一遍";types(groupOrder: 5):用tsc对test/typings/**/*.test-d.ts做类型级断言;e2e(groupOrder: 6):跑test/e2e/**/*.test.ts,模拟真实的pnpm dlx msw initCLI 初始化流程。
此外,浏览器项目统一启用了@vitest/browser-playwright的 Chromium 实例并保持 headless;api.host被固定为127.0.0.1,其注释说明:测试页面与测试服务器同 host 托管(配合vitest.setup.ts),才能保证在 document 上设置的 cookie 能随请求发送到测试服务器。
三、defineTestNetwork:一套定义、双端运行
test/README.md明确指出测试的组织方式:用defineTestNetwork({ handlers })定义初始 handlers,每个测试会拿到实际的setupServer()或setupWorker()实例作为networkfixture。这是整个集成测试体系的"入口 API",其实现位于 test/setup/vitest.ts 与 test/setup/network.ts。
3.1 NetworkDefinition 配置项
test/setup/network.ts定义了NetworkDefinition接口,它决定了一个测试网络如何被创建与启动:
export interface NetworkDefinition { enabled?: boolean handlers?: Array<NetworkHandler> serverOptions?: Parameters<SetupServer['listen']>[0] workerOptions?: StartOptions }各字段含义如下:
handlers:初始请求处理器数组,类型与setupServer().use()的参数一致,即http.get(...)、graphql.query(...)、ws.link(...)等处理器;enabled:是否在测试开始时自动启动网络。默认true;设为false可用于测试"拦截器尚未生效"的场景(见下文 WebSocket 案例);serverOptions:Node 端setupServer().listen()的选项(透传给network.listen());workerOptions:浏览器端setupWorker().start()的选项(透传给network.start())。
3.2 同一份定义如何切换到 Node 与浏览器
startNetwork()是关键的运行时分流逻辑(test/setup/network.ts):
const network = typeof window === 'undefined' ? (await import('msw/node')).setupServer(...handlers) : (await import('msw/browser')).setupWorker(...handlers)通过typeof window === 'undefined'判断当前运行时:Node 下实例化setupServer,浏览器下实例化setupWorker,从而让同一份 handlers 定义在两个环境各得其所。enableNetwork()则通过'close' in network区分实例类型——Node 的setupServer有close()方法,调用network.listen({ onUnhandledFrame: 'bypass', ... });浏览器 worker 则调用network.start()。默认onUnhandledFrame: 'bypass'保证未命中的帧放行到真实网络。同时,networkDefinitions用WeakMap记录每个 network 实例对应的定义,供后续读取。
3.3 network fixture 与自动化的生命周期管理
test/setup/vitest.ts中的defineTestNetwork基于 Vitest 的base.extend做了三件事:
- 注入
testServerfixture(scope: 'worker'):通过inject('testServer')从 Vitest 全局 setup 拿到共享测试服务器的http、https、ws三组 URL,并提供createUrl(path)辅助函数用于构造相对测试服务器的完整 URL; - 注入
networkfixture(scope: 'file',auto: true):文件级自动创建——startNetwork(definition)启动网络,onCleanup中调用stopNetwork()回收; - 每个测试前的状态复原:
test.beforeEach中先从WeakMap读取该 network 的定义(而非闭包),若上个测试把网络stop()掉了(readyState === 0)则重新enableNetwork(),随后调用network.resetHandlers()清空运行时追加的处理器——保证每个用例都从文件定义时的初始状态出发。
一个值得注意的细节:beforeEach中"从getNetworkDefinition(network)读取定义"而非直接读闭包,是因为同文件内多次调用defineTestNetwork()会注册各自的 hook,而 hook 会对文件中所有测试生效;通过 WeakMap 按实例取定义,才能让"每个测试使用自己所属文件定义的状态"。
四、面向浏览器测试的扩展 fixtures
在 Node 中直接fetch即可断言,但浏览器测试需要与真实页面、Service Worker 事件交互。因此 test/setup/vitest-helpers.ts 在基础defineTestNetwork之上又扩展了一批高价值 fixture:
page:浏览器页面对象封装,提供evaluate()(在测试服务器同源环境下执行回调)、waitForRequest()、waitForResponse()(基于response:mocked/response:bypass事件的轮询等待)、waitForTimeout()、url()等能力;fetch:跨环境统一的请求助手。它通过给请求头注入唯一的accept-language值作为请求 ID,监听response:mocked与response:bypass事件来捕获响应,并返回统一的BrowserResponse封装(含status()、json()、text()、headers()、fromServiceWorker()等方法)。浏览器下还会过滤掉accept头含msw/passthrough的透传响应;query:GraphQL 专用请求器,支持GET、POST(JSON body)与 multipart 文件上传三种形态;spyOnConsole:文件级 console 间谍,把log/warn/error/group等方法按类型捕获为ConsoleMessages,供断言 MSW 的日志输出;makeUrl:将相对路径解析为测试服务器绝对 URL。
vitest-helpers.ts的page.beforeEach还会通过 CDP 清空浏览器 cookie、localStorage 与 sessionStorage,保证浏览器用例之间互不污染。
五、共享测试服务器:集成测试的地基
浏览器与 Node 测试都依赖"真实的上游服务器"来验证 bypass、passthrough、CORS、流式响应等行为。这个地基由 vitest.setup.ts 提供:
createSharedTestServer()基于@epic-web/test-server创建 HTTP + HTTPS 双协议服务器,并通过project.provide('testServer', ...)把http、https、ws三组 URL 注入到所有测试项目;- 内置了一批路由 fixture,覆盖典型场景:
/login、/book/:bookId(404 场景)、/range(206 分片响应)、/stream(分块流)、/text-event-stream(SSE 流)、/cors、/passthrough/status-204|205|304/user、/user、/repos/:owner/:name、GraphQL 相关路由等; - 中间件对所有请求回显 CORS 头(而非通配符
*),从而支持credentials: include这类带凭据请求的 CORS 校验; teardown()统一关闭 HTTP 服务器与 WebSocket 服务器。
而 test/support/alias.ts 中的mswExports把msw、msw/node、msw/browser、msw/http、msw/graphql、msw/ws、msw/sse等导出映射到lib/下的构建产物,让测试统一走真实构建后的入口,而非源码直连。
六、从案例看用法
6.1 HTTP:最简的 defineTestNetwork 用法
test/protocols/http/basic.test.ts 展示了标准范式:
import { http, HttpResponse } from 'msw' import { defineTestNetwork, expect } from '../../setup/vitest-helpers' const handlers = [ http.get('https://example.com/users/:username', ({ params }) => { const { username } = params return HttpResponse.json({ name: 'John Maverick', originalUsername: username, }) }), ] const test = defineTestNetwork({ handlers }) test('mocks response to a GET request', async ({ fetch }) => { const response = await fetch('https://example.com/users/octocat') const status = response.status() const body = await response.json() expect(status).toBe(200) expect(response.fromServiceWorker()).toBe(true) expect(body).toEqual({ name: 'John Maverick', originalUsername: 'octocat', }) })这个文件不带node或browser后缀,因此会同时在 Node(setupServer)与浏览器(setupWorker)两个项目中执行——同一份断言验证了库在两种运行时下行为一致。fetchfixture 在不同环境返回的BrowserResponse接口统一,测试体无需关心运行环境。
6.2 WebSocket:用enabled: false验证拦截器未生效
test/protocols/websocket/ws.apply.browser.test.ts 展示了enabled的典型用途:
const api = ws.link('wss://example.com') const handlers = [api.addEventListener('connection', () => {})] const test = defineTestNetwork({ enabled: false, handlers }) test('does not apply the interceptor until "worker.start()" is called', async ({ network }) => { if (!('start' in network)) { throw new Error('Expected a browser worker instance') } expect(new WebSocket('wss://example.com').constructor.name).toBe('WebSocket') await network.start() expect(new WebSocket('wss://example.com').constructor.name).not.toBe('WebSocket') })由于该测试需要network.start()的浏览器 worker 能力,它使用browser.test.ts后缀只跑浏览器端,并用'start' in network做运行时守卫。enabled: false让网络在测试开始时不被自动启动,从而可以断言"调用start()之前原生WebSocket未被替换,调用之后被替换为 MSW 的拦截实现"。
6.3 多标签页与整页生命周期:pw.test.ts 的用武之地
需要 Playwright 的场景集中在test/msw-api/setup-worker/下,例如:
test/msw-api/setup-worker/stop/stop-message.browser.test.ts:验证停止 worker 时发送给 worker 的终止消息;test/msw-api/setup-worker/stop/removes-all-listeners.pw.test.ts:验证stop()后监听器被彻底移除;test/msw-api/setup-worker/start/start.pw.test.ts与find-worker.pw.test.ts:验证整页启动流程与 worker 查找;test/msw-api/setup-worker/stop/stop-multiple-tabs.pw.test.ts:多标签页场景。
这些用例因为涉及页面导航、Service Worker 注册与多标签页协同,无法在 Vitest Browser Mode 的单一页面模型下完成,故使用 Playwright 驱动真实浏览器(配置见 test/playwright.config.ts)。
七、如何运行与新增测试
test/README.md将运行与新增测试的细节指向 CONTRIBUTING.md。结合仓库配置,可归纳出以下实践要点:
- 运行:仓库使用 pnpm + Vitest(
pnpm-workspace.yaml、pnpm-lock.yaml位于仓库根目录)。vitest.config.ts中的projects会按groupOrder依次执行 unit → node → memory → memory-browser → browser → types → e2e 各阶段; - 新增测试的选型决策:
- 断言纯 Node 行为(如
setupServer在 Node 下的网络拦截)→*.node.test.ts; - 断言浏览器专属行为(如 Service Worker、
window相关逻辑)→*.browser.test.ts; - 需要两端同时验证的通用协议行为 → 不带后缀的
*.test.ts(让它在 node 与 browser 两个项目各跑一遍); - 涉及整页导航、多标签页或 worker 生命周期 →
*.pw.test.ts(Playwright); - 度量内存泄漏 →
*.memory.test.ts/*.browser.memory.test.ts。
- 断言纯 Node 行为(如
- 通用模板:定义
handlers→ 调用defineTestNetwork({ handlers })(必要时传enabled、serverOptions、workerOptions)→ 在测试回调中使用network、fetch、page、testServer等 fixture 完成请求与断言。
结语
从 test/README.md 的四条命名规范,到defineTestNetwork的运行时分流、NetworkDefinition配置、自动化的生命周期管理,再到共享测试服务器与 Playwright 补充,MSW 的集成测试体系是一条"一份定义、双端执行、按需分流"的完整流水线。理解这套机制,不仅能让外部贡献者准确地把新测试放到正确的位置并选对后缀,也能为自建库的集成测试设计提供一套经过大规模协议测试验证的参考范式。
- 测试
- 开发工具
- 前端
【免费下载链接】msw
The industry standard for API mocking in JavaScript.
相关推荐
AgentsView 中 Codex 转录的增量检查点与流式全量导入设计
AgentsView 中 Codex 转录的增量检查点与流式全量导入设计 AgentsView 将 Claude Code、Codex 等编码 Agent 的本
测试开发工具前端如何高效处理Bowtie2的大数据集?内存管理与并行计算终极指南
如何高效处理Bowtie2的大数据集?内存管理与并行计算终极指南 Bowtie2作为一款快速且内存高效的序列比对工具,在处理大规模基因组数据时表现卓越。本文将深
终极指南:如何在Windows上使用grepWin正则表达式工具快速搜索替换文件内容
终极指南:如何在Windows上使用grepWin正则表达式工具快速搜索替换文件内容 还在为Windows系统中海量文件的文本搜索和替换而烦恼吗?grepWin
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考