前端测试策略: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/react | React 组件渲染与交互测试 |
| @testing-library/user-event | 用户行为模拟(点击、输入) |
| jsdom | DOM 环境模拟 |
配置文件:vitest.config.ts
MSW 模拟 Tauri IPC
Tauri 应用的前端通过 @tauri-apps/api 的 invoke() 调用后端命令。测试环境中没有 Tauri 后端,因此需要模拟。
MSW 的工作原理:
测试代码 ↓ 发起 invoke('get_providers', { app_type: 'claude' })MSW Handler ↓ 匹配命令名称 ↓ 返回 mock 数据 { providers: { ... } }测试代码 ↓ 收到 mock 数据,验证 UI 渲染MSW Handler 示例结构:
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() 函数本身:
- MSW 拦截的是网络层(或 jsdom 中的 fetch 调用),模拟的是 Tauri IPC 的 HTTP-like 接口
- 前端代码不需要为测试做特殊适配——测试和生产的代码路径一致
- 可以测试完整的请求/响应生命周期,包括超时、错误响应等
测试目录结构
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(这是集成测试/端到端测试的职责)
运行命令
# 运行所有测试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/react的act()包裹动画触发
设计取舍
MSW vs 直接 mock invoke
| 方案 | 优势 | 劣势 |
|---|---|---|
| MSW | 测试真实的网络流、支持延迟模拟、错误场景 | 配置复杂、需要理解 IPC 协议 |
| Mock invoke 函数 | 简单直接 | 测试路径与生产路径不同 |
| 启动真实 Tauri 后端 | 最真实 | 慢、需要 Rust 环境、难以模拟错误 |
CC Switch 选择 MSW 的原因:它可以在不启动 Tauri 后端的情况下测试完整的前端行为,包括错误处理、加载状态、重试逻辑等。
为什么不做端到端测试?
端到端测试(如 Playwright)需要启动完整的 Tauri 应用,这意味着:
- CI 中需要 Rust 编译环境
- 测试速度慢(启动应用需要数秒)
- 难以模拟特定的错误场景(如数据库损坏)
CC Switch 目前的策略是:前端 MSW 测试 + 后端 Rust 单元测试,两者结合覆盖了绝大多数场景。
参考来源
- vitest 文档. https://vitest.dev/
- MSW 文档. https://mswjs.io/
- Testing Library. https://testing-library.com/