如何完美测试 AI 摘要通过 SSE 输出的打字效果?

161 阅读5分钟

前面两篇文章简单介绍了如何只用 Nextjs 在本地模拟 SSE 实现打字效果和通过 Streamdown 优雅地解析输出 AI 返回的 Markdown 文本,接下来就是如何测试了。

Mock 框架

Mock 框架有很多,个人倾向使用 Mock Service Worker (MSW) 来测 AI 相关的功能,在下面的详细对比中也有意将 MSW 放在第一位。

  • MSW (Mock Service Worker)
  • JSON Server
  • WireMock
  • Nock
  • MirageJS

框架对比

1. 架构设计与工作原理

框架工作原理优点缺点
MSW基于 Service Worker API 拦截网络请求• 在网络层面拦截,透明度高
• 支持浏览器和 Node.js 环境
• 不侵入业务代码
• 需要理解 Service Worker 概念
• 在某些环境下配置相对复杂
JSON Server启动独立的 REST API 服务器• 零配置快速启动
• 自动生成 CRUD 接口
• 支持关系数据和查询
• 仅限于 REST API
• 功能相对简单
• 需要独立端口
WireMock基于 HTTP 服务器的 Mock 框架• 功能强大,支持复杂场景
• 丰富的匹配规则
• 支持状态管理和故障注入
• 主要面向 Java 生态
• 配置复杂
• 资源消耗较大
NockHTTP 请求拦截库• 专门针对 Node.js 环境
• API 简洁直观
• 测试友好
• 仅支持 Node.js
• 不支持浏览器环境
• 功能相对单一
MirageJS客户端服务器模拟框架• 提供完整的数据层模拟
• 支持数据库关系
• 内置 ORM
• 学习曲线陡峭
• 配置复杂
• 体积较大

2. 开发体验与易用性

框架学习成本配置复杂度API 设计TypeScript 支持热重载调试体验
MSW中等中等声明式,直观⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
JSON Server极低极简约定优于配置⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
WireMock复杂功能丰富但冗长⭐⭐⭐⭐⭐⭐⭐⭐⭐
Nock简单链式调用,简洁⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
MirageJS复杂ORM 风格⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐

详细说明:

  • MSW: 声明式 API 设计优秀,TypeScript 支持完善,但 Service Worker 调试相对复杂
  • JSON Server: 学习成本最低,一行命令启动,但功能相对基础
  • WireMock: 企业级功能完善,但配置复杂,主要面向 Java 开发者
  • Nock: API 简洁明了,测试友好,但仅限 Node.js 环境
  • MirageJS: 功能最全面,但学习曲线陡峭,配置最复杂

3. 性能表现

框架启动速度内存占用响应延迟并发处理
MSW⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
JSON Server⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
WireMock⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
Nock⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
MirageJS⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐

4. 功能覆盖度

功能特性MSWJSON ServerWireMockNockMirageJS
REST API
GraphQL
WebSocket/SSE
浏览器环境
Node.js 环境
请求录制回放
故障注入
延迟模拟
状态管理基础
数据持久化
数据关系建模基础
自定义中间件
代理模式
TypeScript 支持部分部分
热重载

符号说明:

  • ✅:完全支持
  • 部分:部分支持
  • 基础:基础功能
  • ❌:不支持

5. 生态系统与社区支持

框架GitHub StarsNPM 周下载量社区活跃度文档质量维护状态
MSW15.8k+1.2M+⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐积极维护
JSON Server72k+800k+⭐⭐⭐⭐⭐⭐⭐⭐稳定维护
WireMock6.2k+N/A⭐⭐⭐⭐⭐⭐⭐⭐⭐积极维护
Nock12.6k+1.8M+⭐⭐⭐⭐⭐⭐⭐⭐稳定维护
MirageJS5.4k+50k+⭐⭐⭐⭐⭐⭐⭐维护缓慢

适合场景

🚀 适合 MSW 的场景:

  • 现代前端应用开发(React、Vue、Angular)
  • 同时支持开发和测试环境
  • 模拟实时数据流(SSE、WebSocket)
  • 统一的 Mock 解决方案

⚡ 适合 JSON Server 的场景:

  • 快速原型验证
  • 简单的 REST API Mock
  • 最小学习成本
  • 独立的 Mock 服务

🏢 适合 WireMock 的场景:

  • 企业级功能(录制回放、故障注入)
  • 微服务架构测试
  • 复杂的网络场景模拟
  • Java 技术栈集成

🧪 适合 Nock 的场景:

  • Node.js 后端测试
  • 精确的请求拦截验证
  • 轻量级测试解决方案
  • 与测试框架深度集成

🎯 适合 MirageJS 的场景:

  • 完整的数据层模拟
  • 复杂的数据关系建模
  • 长期前端项目开发
  • 离线开发能力

选择 MSW 的优势

  1. 架构先进: 基于 Service Worker 设计,在网络层面拦截请求,不侵入业务代码
  2. 跨环境支持: 同时支持浏览器和 Node.js 环境,一套代码多环境复用
  3. 开发体验: 优秀的 TypeScript 支持和现代化的 API 设计
  4. 功能完善: 支持 REST、GraphQL、实时数据流等多种协议
  5. 社区活跃: 持续的更新和丰富的生态系统

安装 MSW

演示基于 Nextjs

  • 安装 MSW
// npm
npm i msw --save-dev

// yarn
yarn add msw -D
  • 额外的一步:生成浏览器端的 ./public/mockServiceWorker.js
npx msw init public/

使用 MSW

目录结构和文件解释

MSW SSE Mock 项目 - 目录结构
===============================

项目根目录: /mock-sse-by-msw
├── 📁 public/                        # 静态文件目录
│   └── 📄 mockServiceWorker.js       # MSW Service Worker 脚本,拦截浏览器请求
├── 📁 src/                           # 源代码目录
│   ├── 📁 app/                       # Next.js App Router 目录
│   │   ├── 📄 globals.css            # 全局 CSS 样式,应用于整个应用程序
│   │   ├── 📄 layout.tsx             # 根布局组件,包住所有页面
│   │   └── 📄 page.tsx               # 主页面组件,展示 SSE 功能演示
│   ├── 📁 components/                # 可复用的 React 组件目录
│   │   └── 📄 mockServer.tsx         # Mock 服务器初始化组件
│   └── 📁 mock/                      # MSW Mock 配置和处理器
│       ├── 📁 __fixture__/           # 测试数据目录
│       │   └── 📄 summaryTexts.js    # SSE 流式传输模拟的示例文本数据
│       ├── 📄 browser.ts             # MSW 浏览器端设置,用于拦截客户端请求
│       ├── 📄 handler.ts             # MSW 请求处理器,定义 Mock API
│       ├── 📄 initmock.ts            # 开发环境 Mock 初始化逻辑
│       └── 📄 server.ts              # MSW 服务器端设置,用于 Node.js 环境(测试)
└── 📄 yarn.lock                      # Yarn 依赖锁定文件,用于包管理

开发工作流

  1. MSW Service Worker 在浏览器中拦截网络请求
  2. Mock 处理器提供真实的 API 响应
  3. SSE 端点逐字符流式传输数据
  4. React 组件消费实时数据流
  5. 开发服务器提供热重载即时更新
    • 更改 ./src/mock/__fixture__/summaryText.js 中的 Markdown 数据

核心代码

在 MSW 中模拟 SSE (Server-Sent Events) handler

// ./src/mock/handler.ts
export const handlers = [
  
  // SSE (Server-Sent Events) handler
  http.get("/api/sse", ({ request }) => {
    const url = new URL(request.url);
    const interval =20;
    const summaryKey = url.searchParams.get('summary');
    const maxCount = summaryKey && summaryArray[summaryKey].length || 10;
    
    const stream = new ReadableStream({
      start(controller) {
        let counter = 0;
        
        const sendEvent = () => {
          let message = "";
          
          // If summary key is provided and exists in summaryArray, use summary text
          if (summaryKey && summaryArray[summaryKey] && summaryArray[summaryKey][counter]) {
            message = summaryArray[summaryKey][counter];
          }
          
          const data = {
            id: counter,
            message,
            timestamp: new Date().toISOString(),
            type: 'update',
          };
          
          const eventData = `data: ${JSON.stringify(data)}\n\n`;
          controller.enqueue(new TextEncoder().encode(eventData));
          
          counter++;
          
          if (counter < maxCount) {
            setTimeout(sendEvent, interval); // Send event with custom interval
          } else {
          
            // Send final event and close
            controller.enqueue(new TextEncoder().encode('data: {"type":"close","message":"Stream ended"}\n\n'));
            controller.close();
          }
        };
        
        // Send initial event
        sendEvent();
      }
    });
    
    return new HttpResponse(stream, {
      status: 200,
      headers: {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive',
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Headers': 'Cache-Control'
      }
    });
  })
]

复杂格式 Markdown 的输出效果

内置的 CSS 样式GitHub 风格的 MarkdownCode Block
image.pngimage.pngimage.png
复杂的数学公式未闭合的 MarkdownMermaid Diagram
image.pngimage.pngimage.png

代码仓库

Check out github.com/itpretty/mo… and have fun as always :)