首页
/ OpenHands 前端 WebSocket 测试实践:基于 __tests__/helpers 的可复用测试组件与 MSW 模拟搭建

OpenHands 前端 WebSocket 测试实践:基于 __tests__/helpers 的可复用测试组件与 MSW 模拟搭建

2026-09-04 20:19:45作者:农烁颖Land

OpenHands 前端(React + Vite)中有大量围绕 WebSocket 的交互逻辑——事件流接收、消息乐观更新、连接状态展示、错误提示——这些逻辑很难通过普通的服务层 mock 验证。本仓库在 tests/helpers 目录中沉淀了一套可复用的 WebSocket 测试工具:一组把 Zustand store 与 Context 状态“渲染出来”的测试组件,以及一套基于 MSW(Mock Service Worker)的 WebSocket 模拟搭建函数。读完本文,你能够掌握:如何用“显示型测试组件”把难以断言的 store 状态转成 DOM 断言,以及如何用 MSW 2.x 的 ws.link API 在 Vitest 中搭建可控的 WebSocket 服务器来验证事件处理链路。

目录结构:一个只服务测试的 helpers 目录

__tests__/helpers/ 目录下只有两个核心文件,均与前端测试套件直接相关:

这两个文件共同覆盖了 WebSocket 测试的两端:一端是“被测代码”(WebSocket 客户端与 store 更新逻辑),另一端是“被测结果的读取”(通过渲染组件 + data-testid 断言 DOM)。

显示型测试组件:把 store 状态变成可断言的 DOM

websocket-test-components.tsx 导出了 4 个组件,每个组件订阅一个真实的 Context 或 Zustand store,并把关键状态渲染为带 data-testid 的 DOM 节点。测试侧可以用 Testing Library 直接 getByTestId 读取数值,从而把“内存中的 store 状态”转成“可断言的 DOM 文本”。这种模式的价值在于:它复用了生产代码里真正的 Context 与 store,而不是重新实现一份假的。

ConnectionStatusComponent —— 连接状态

export function ConnectionStatusComponent() {
  const context = useConversationWebSocket();

  return (
    <div>
      <div data-testid="connection-state">
        {context?.connectionState || "NOT_AVAILABLE"}
      </div>
    </div>
  );
}

该组件通过 src/contexts/conversation-websocket-context.tsx 导出的 useConversationWebSocket Hook 读取当前 WebSocket 连接状态。注意其兜底逻辑:Context 不可用时渲染 "NOT_AVAILABLE",这样在 Provider 缺失的测试场景下也能得到确定性输出,而不是空白。

EventStoreComponent —— 事件 store 快照

export function EventStoreComponent() {
  const { events, uiEvents } = useEventStore();
  return (
    <div>
      <div data-testid="events-count">{events.length}</div>
      <div data-testid="ui-events-count">{uiEvents.length}</div>
      <div data-testid="latest-event-id">
        {isAgentServerEvent(events[events.length - 1])
          ? (events[events.length - 1] as OpenHandsEvent).id
          : "none"}
      </div>
    </div>
  );
}

它一次性暴露三个断言点:

  • events-count:事件 store 中事件的总数;
  • ui-events-count:UI 事件的数量;
  • latest-event-id:最后一条事件若是 Agent Server 事件(通过 src/types/agent-server/type-guards 中的 isAgentServerEvent 类型守卫判断),则显示其 id,否则显示 "none"

其中“最新事件 ID”这一项对 WebSocket 测试特别有用:当模拟服务器推入一条带 id 的事件后,测试只需断言 latest-event-id 的值,就能确认事件被完整消费并写入了 store,无需深入遍历整个数组。

OptimisticUserMessageStoreComponent —— 乐观消息队列

export function OptimisticUserMessageStoreComponent() {
  const { pendingMessages } = useOptimisticUserMessageStore();
  return (
    <div>
      <div data-testid="optimistic-user-message">
        {pendingMessages[0]?.text || "none"}
      </div>
      <div data-testid="optimistic-user-message-count">
        {pendingMessages.length}
      </div>
      <div data-testid="optimistic-user-message-statuses">
        {pendingMessages.map((m) => m.status).join(",") || "none"}
      </div>
    </div>
  );
}

OpenHands 在用户发送消息后、服务端回显之前,会先在本地维护一个“乐观消息”队列。这个组件把队列的头部文本、长度和全部状态(逗号拼接)都渲染出来,因此测试可以覆盖三类典型场景:发送后消息进入 pending、服务端回显后队列清空、以及状态流转(如从 pending 变为 sent)。

ErrorMessageStoreComponent —— 错误信息

export function ErrorMessageStoreComponent() {
  const { errorMessage } = useErrorMessageStore();
  return (
    <div>
      <div data-testid="error-message">{errorMessage || "none"}</div>
    </div>
  );
}

用于断言连接失败或事件处理失败时,错误信息 store 被正确写入(或保持为 "none")。

四个组件的 data-testid 汇总如下,可直接用于测试断言:

组件 data-testid 含义
ConnectionStatusComponent connection-state WebSocket 连接状态,缺省显示 NOT_AVAILABLE
EventStoreComponent events-count / ui-events-count / latest-event-id 事件总数 / UI 事件数 / 最新 Agent Server 事件 ID
OptimisticUserMessageStoreComponent optimistic-user-message / optimistic-user-message-count / optimistic-user-message-statuses 首条待回显消息文本 / 队列长度 / 各消息状态
ErrorMessageStoreComponent error-message 当前错误信息,无错误时显示 none

MSW WebSocket 模拟工具:msw-websocket-setup.ts

msw-websocket-setup.ts 基于 MSW 2.x(本仓库锁定版本为 2.15.0,见 package.json)的 ws.link API 封装了 4 个工具函数,形成“link → server → setup → 场景预设”的分层结构。

第一层:createWebSocketLink

export const createWebSocketLink = (url = "ws://localhost/events/socket") =>
  ws.link(url);

ws.link(url) 创建一个指向目标 URL 的 MSW WebSocket 链接,测试中可以向该 link 注册事件监听,模拟服务器向客户端推送数据。默认 URL 为 ws://localhost/events/socket,可传参覆盖。

第二层:createWebSocketMockServer

export const createWebSocketMockServer = (wsLink: ReturnType<typeof ws.link>) =>
  setupServer(
    wsLink.addEventListener("connection", ({ server }) => {
      server.connect();
    }),
  );

这里用 setupServer 创建一个仅服务于 WebSocket 的 MSW 服务器,并注册了一个 connection 监听器:每当客户端连接到这个 link,就调用 MSW 提供的 server.connect(),让服务器侧也完成握手。这一步是 WebSocket 模拟能跑通的关键——HTTP mock 是“请求-响应”模型,而 WebSocket 是长连接,必须显式完成双方连接。

第三层:createWebSocketTestSetup

export const createWebSocketTestSetup = (
  url = "ws://localhost/events/socket",
) => {
  const wsLink = createWebSocketLink(url);
  const server = createWebSocketMockServer(wsLink);
  return { wsLink, server };
};

把前两层组合成一个完整搭建,返回 { wsLink, server },测试文件中通常用它替代手写的 ws.link + setupServer 样板代码。

场景预设:conversationWebSocketTestSetup

export const conversationWebSocketTestSetup = () =>
  createWebSocketTestSetup("ws://localhost:3000/sockets/events/*");

这是专为“会话级 WebSocket handler 测试”准备的标准入口。源码注释明确说明:它采用 V1 版本的 URL 模式 /sockets/events/{conversationId},并使用通配符 * 匹配任意会话 ID。这意味着无论测试中切换到哪个 conversation,只要 URL 前缀一致,mock 就能命中,无需为每个会话 ID 单独创建 link。

与全局 MSW 服务器的关系

需要注意的是,本仓库的 Vitest 全局配置 vitest.setup.ts 已经基于 src/mocks/node.ts 导出的 server 管理了 HTTP mock 的生命周期(listen / resetHandlers / close)。MSW 指南 中也明确建议:HTTP 请求应优先用 server.use() 复用全局服务器,而“需要真实网络层行为(WebSocket、重试逻辑等)的测试”才走网络层模拟,并指引开发者到 __tests__/helpers/msw-websocket-setup.ts 查找工具函数。也就是说,这套 helper 定位就是 MSW 体系中“WebSocket 网络层测试”的专用入口,与全局 HTTP mock 服务器并存、各司其职。

在测试中使用:从 helpers 到断言

README 中给出的标准用法如下(导入路径按仓库内实际位置书写):

import {
  ConnectionStatusComponent,
  EventStoreComponent,
} from "./__tests__/helpers/websocket-test-components";
import { conversationWebSocketTestSetup } from "./__tests__/helpers/msw-websocket-setup";

// Set up MSW server
const { wsLink, server } = conversationWebSocketTestSetup();

// Render components with WebSocket context (helper function defined in test file)
renderWithWebSocketContext(<ConnectionStatusComponent />);

典型的使用模式分四步:

  1. beforeEach 中调用 conversationWebSocketTestSetup() 拿到 wsLinkserver,并 server.listen()
  2. 渲染被测的 WebSocket Provider,同时渲染 websocket-test-components.tsx 中的显示组件;
  3. 通过 wsLink 模拟服务器推送事件(向客户端发送 JSON 消息),或等待客户端发送消息;
  4. getByTestId("events-count") 等断言 DOM 中的 store 快照值,最后 server.close()

仓库中的 conversation-websocket-context.test.tsx 展示了当前测试套件的另一种风格:直接 vi.mockuseWebSocket 传输层,手动触发 onMessage 回调来驱动真实 Provider 与事件 store。从源码结构看,随着 WebSocket 传输实现(V1 URL 模式、重连参数等)的演进,仓库对 WebSocket 的测试策略在“mock 传输层”与“网络层 MSW 模拟”之间并存选择——helper 目录提供的是后者所需的基础设施。use-websocket.test.ts 中也能看到直接使用 ws.link("ws://acme.com/ws")setupServer 的网络层测试写法,与 helper 封装的形态一致。

为什么抽出这套 helpers

README 总结了四点收益,结合仓库现状逐条可以印证:

  • 可复用(Reusability):显示组件与 MSW 搭建函数被设计为跨测试文件共享。显示组件不依赖任何特定测试数据,任何需要观察这四个 store 的测试都可以直接复用;
  • 可维护(Maintainability):WebSocket mock 的搭建逻辑(默认 URL、connection 握手)集中在一处。例如 V1 URL 模式从旧路径切换到 /sockets/events/{conversationId} 通配模式时,只需修改 msw-websocket-setup.tsconversationWebSocketTestSetup 的一行,而不必逐个测试文件改 URL;
  • 一致性(Consistency):不同 WebSocket 相关测试使用同一套默认 URL 与服务器配置,避免了各测试文件手写 setupServer 时的细微差异导致的行为不一致;
  • 可读性(Readability):测试文件从搭建样板中解放出来,聚焦于“推入什么事件、断言什么状态”这一核心测试逻辑。

参考文件

文件 说明
tests/helpers/README.md helpers 目录说明(本文依据的原始文档)
tests/helpers/websocket-test-components.tsx 4 个显示型测试组件实现
tests/helpers/msw-websocket-setup.ts MSW WebSocket 模拟工具函数
tests/MSW.md 项目 MSW 使用指南,含服务层 vs 网络层 mock 的选型建议
vitest.setup.ts Vitest 全局 MSW 生命周期配置
tests/contexts/conversation-websocket-context.test.tsx WebSocket 上下文测试的实际用例
tests/hooks/use-websocket.test.ts 使用 ws.link 网络层模拟的 Hook 测试

适用前提与限制:本文所述 API 基于 MSW 2.15.0ws.link / setupServer 接口与 Vitest 环境,WebSocket mock 依赖 Node 端的请求拦截能力,仅在单元测试(vitest)场景可用;浏览器开发模式的 mock 走的是 src/mocks/browser.ts 的 Service Worker 路径,与本文讨论的测试工具不直接相关。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
980
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384