跳转到内容

前端测试策略:vitest + MSW

前端测试策略:vitest + MSW

学习目标

  • 理解 MSW 如何模拟 Tauri IPC API 调用
  • 掌握 CC Switch 前端测试的三层结构(单元测试、组件测试、集成 mock)
  • 了解测试隔离与数据清理策略
  • 识别 Tauri 前端测试的特殊挑战

项目实践

测试技术栈

CC Switch 的前端测试使用:

工具用途
vitest测试框架,兼容 Jest API
MSW (Mock Service Worker)模拟 Tauri API 调用
@testing-library/reactReact 组件渲染与交互测试
@testing-library/user-event用户行为模拟(点击、输入)
jsdomDOM 环境模拟

配置文件:vitest.config.ts

MSW 模拟 Tauri IPC

Tauri 应用的前端通过 @tauri-apps/apiinvoke() 调用后端命令。测试环境中没有 Tauri 后端,因此需要模拟。

MSW 的工作原理

测试代码
↓ 发起 invoke('get_providers', { app_type: 'claude' })
MSW Handler
↓ 匹配命令名称
↓ 返回 mock 数据 { providers: { ... } }
测试代码
↓ 收到 mock 数据,验证 UI 渲染

MSW Handler 示例结构

mocks/handlers.ts
import { http, HttpResponse } from 'msw'
export const handlers = [
http.post('ipc://get_providers', async ({ request }) => {
const body = await request.json()
return HttpResponse.json({
providers: {
'default-claude': {
id: 'default-claude',
name: 'Default (Claude)',
category: 'official',
settings_config: { env: {} }
}
}
})
}),
http.post('ipc://switch_provider', async () => {
return HttpResponse.json({ success: true })
}),
// ... 更多命令的 mock handlers
]

为什么模拟 ipc:// 而非 invoke() 函数本身

  1. MSW 拦截的是网络层(或 jsdom 中的 fetch 调用),模拟的是 Tauri IPC 的 HTTP-like 接口
  2. 前端代码不需要为测试做特殊适配——测试和生产的代码路径一致
  3. 可以测试完整的请求/响应生命周期,包括超时、错误响应等

测试目录结构

tests/
├── mocks/
│ ├── handlers.ts # MSW handlers (按命令组织)
│ └── server.ts # MSW server 启动/停止
├── components/
│ ├── providers/ # 供应商组件测试
│ ├── mcp/ # MCP 面板测试
│ └── settings/ # 设置面板测试
└── hooks/
└── useProviderActions.test.ts # Hook 测试

测试隔离

每个测试文件独立启动和停止 MSW server:

import { setupServer } from 'msw/node'
import { handlers } from '../mocks/handlers'
const server = setupServer(...handlers)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers()) // 重置每个测试后的 handler 状态
afterAll(() => server.close())

resetHandlers() 的重要性:避免测试之间的 mock 状态泄漏。如果测试 A 修改了某个 handler 的返回值(如模拟错误),测试 B 不应受到影响。

组件测试模式

CC Switch 的组件测试遵循 testing-library 的最佳实践:

test('显示供应商列表', async () => {
render(<ProviderList />)
// 等待异步数据加载
await waitFor(() => {
expect(screen.getByText('Default (Claude)')).toBeInTheDocument()
})
// 验证交互
await userEvent.click(screen.getByRole('button', { name: '启用' }))
// 验证结果
await waitFor(() => {
expect(screen.getByText('已启用')).toBeInTheDocument()
})
})

测试不验证的

  • Tauri 后端是否正确执行了命令(这是后端测试的职责)
  • 数据是否真正写入了 SQLite(这是集成测试/端到端测试的职责)

运行命令

Terminal window
# 运行所有测试
pnpm test:unit
# Watch 模式(开发推荐)
pnpm test:unit:watch
# 带覆盖率报告
pnpm test:unit --coverage

问题与规避

陷阱 1:MSW 版本与 Tauri IPC 的兼容性

问题:MSW 最初设计为拦截 HTTP 请求,而 Tauri IPC 不是标准的 HTTP。MSW 2.x 的 API 变化可能影响模拟逻辑。

CC Switch 的解决方案

  • 锁定 MSW 版本(package.json 中精确版本)
  • 使用 http.post('ipc://...') 格式模拟 Tauri 的 IPC 调用(Tauri 内部将 invoke 转换为类似 HTTP 的调用)

陷阱 2:TanStack Query 缓存导致测试不稳定

问题:CC Switch 使用 TanStack Query 做数据缓存。如果测试不清空缓存,后续测试可能读到前一个测试的缓存数据。

CC Switch 的解决方案

  • 每个测试使用独立的 QueryClient 实例
  • beforeEach 中重置 QueryClient 缓存
  • 或使用 setQueryData 预填充测试数据,而非依赖 MSW 的异步响应

陷阱 3:Framer Motion 动画导致测试超时

问题:CC Switch 使用 Framer Motion 做动画。testing-library 的 waitFor 可能因为动画未完成而超时。

CC Switch 的解决方案

  • 测试环境中禁用 Framer Motion 动画(通过 mock framer-motion 模块)
  • 或使用 @testing-library/reactact() 包裹动画触发

设计取舍

MSW vs 直接 mock invoke

方案优势劣势
MSW测试真实的网络流、支持延迟模拟、错误场景配置复杂、需要理解 IPC 协议
Mock invoke 函数简单直接测试路径与生产路径不同
启动真实 Tauri 后端最真实慢、需要 Rust 环境、难以模拟错误

CC Switch 选择 MSW 的原因:它可以在不启动 Tauri 后端的情况下测试完整的前端行为,包括错误处理、加载状态、重试逻辑等。

为什么不做端到端测试?

端到端测试(如 Playwright)需要启动完整的 Tauri 应用,这意味着:

  • CI 中需要 Rust 编译环境
  • 测试速度慢(启动应用需要数秒)
  • 难以模拟特定的错误场景(如数据库损坏)

CC Switch 目前的策略是:前端 MSW 测试 + 后端 Rust 单元测试,两者结合覆盖了绝大多数场景。


参考来源