测试
测试理念、分层策略与本项目 Vitest 测试方案
为什么测试
代码测试从来不只是「验证代码没有 bug」。它的本质是:为代码的行为立契约。
一个有测试的函数,不仅告诉你它能跑通,更告诉你:
- 它在哪些输入下做什么
- 它的边界条件在哪里
- 当有人改动它时,哪些行为不能被破坏
这份契约在 AI 时代变得比以往任何时候都重要。
AI 时代,测试的角色变了
AI 很擅长写「看起来对的代码」。它能快速生成符合语法、结构合理、逻辑流畅的实现。但 AI 不了解你的业务约束,不知道某个边界值背后藏着什么历史决策,也不会在意某个副作用是否符合你的领域规则。
没有测试的项目里,AI 放大的是风险。
你需要用极高的心智负担去 review 每一行生成的代码,逐字核实它的正确性。这和「让 AI 帮你干活」的初衷背道而驰。
有测试的项目里,AI 放大的是效率。
测试是客观的裁判。AI 写完代码,跑一遍测试,结果一目了然——通过意味着行为契约被满足,失败意味着 AI 踩到了约束边界。你的 review 可以真正聚焦在「方向对不对」「架构合不合理」,而不是「这段逻辑有没有漏洞」。
本项目推荐的 AI 协作工作流
整理 / 澄清需求
↓
与 AI 探讨实现方案(架构、边界、数据流)
↓
让 AI 起草 / 修改技术文档
↓
加载规范 skill,让 AI 写代码
↓
跑测试 ← 测试是裁判,不是人
↓
git review(聚焦意图与架构,不再逐行验证正确性)
↓
有明显问题 → 指出,让 AI 修改 → 循环测试在这个流程里扮演的角色是自动化验收。它不是开发后的附加步骤,而是整个协作循环得以快速运转的前提。
测试分层策略
前端测试通常分三层,职责不同,成本各异:
| 层级 | 工具 | 覆盖什么 | 不覆盖什么 | 运行时机 |
|---|---|---|---|---|
| 单元测试 | Vitest | 纯函数、hooks、算法、工具库 | UI 渲染、用户流程 | 每次提交 |
| 集成测试 | Vitest + MSW | 模块间协作、API 边界、状态流转 | 浏览器行为、E2E 路径 | 每次提交 |
| E2E 测试 | Playwright | 用户关键操作路径 | 内部逻辑细节 | CI / PR |
三层并不是都需要写满。优先保证单元测试的覆盖密度,对纯逻辑代码而言,单测的反馈最快、成本最低、价值最高。集成测试和 E2E 聚焦在真正重要的业务路径上,不做无意义的覆盖率堆砌。
单元测试
针对没有外部依赖的最小单元:纯函数、自定义 hooks、工具方法、状态计算逻辑。
写单测的核心原则:描述行为,而非实现。测试用例的标题应该是一句对行为的陈述,而不是对代码路径的描述。
// 工具函数单测示例
describe('toArray', () => {
it('null 应返回空数组', () => {
expect(toArray(null)).toEqual([]);
});
it('单个值应包裹为数组', () => {
expect(toArray(1)).toEqual([1]);
});
it('数组应原样返回', () => {
const arr = [1, 2, 3];
expect(toArray(arr)).toBe(arr);
});
});// React hook 单测示例(使用 renderHook)
describe('useLoading', () => {
it('默认 loading 应为 false', () => {
const { result } = renderHook(() => useLoading());
expect(result.current.loading).toBe(false);
});
it('startLoading 应设置为 true', () => {
const { result } = renderHook(() => useLoading());
act(() => result.current.startLoading());
expect(result.current.loading).toBe(true);
});
});集成测试
针对跨模块协作边界:HTTP 请求层与业务逻辑的交互、状态管理与副作用的联动、依赖外部服务的模块。
集成测试中推荐使用 MSW(Mock Service Worker)来拦截网络请求,而不是 mock 函数——这样测试的是真实的 fetch/axios 调用路径,而非 mock 的返回值。
// MSW 集成测试示例(拦截 API,验证业务逻辑)
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
const server = setupServer(
http.get('/api/users', () => {
return HttpResponse.json([{ id: 1, name: 'Alice' }]);
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
it('应正确加载用户列表', async () => {
const users = await fetchUsers();
expect(users).toHaveLength(1);
expect(users[0].name).toBe('Alice');
});E2E 测试
针对用户视角的关键路径:登录流程、权限跳转、核心业务操作(表单提交、数据增删改查)。
E2E 不追求覆盖率,只覆盖如果这条路径挂了,用户会立刻感知到的流程。
// Playwright E2E 示例
import { test, expect } from '@playwright/test';
test('用户应能正常登录并进入首页', async ({ page }) => {
await page.goto('/login');
await page.fill('[name=username]', 'admin');
await page.fill('[name=password]', '123456');
await page.click('[type=submit]');
await expect(page).toHaveURL('/home');
});本项目当前方案
工具链
| 职责 | 工具 | 说明 |
|---|---|---|
| 单元测试 / 集成测试 | Vitest | 与 Vite 同生态,TS 零配置,速度极快 |
| React hook 测试 | @testing-library/react | renderHook + act |
| 覆盖率报告 | @vitest/coverage-v8 | 通过 pnpm test:coverage 生成 |
| API Mock(待接入) | MSW | 已安装,可用于集成测试 |
配置架构
vitest.config.ts ← 根配置,统一管理所有 packages 的测试
packages/@skyroc/config/vitest ← 共享配置(provider、exclude、环境等)
packages/hooks/vitest.config.ts ← 子包配置(继承共享配置)
packages/@core/*/vitest.config.ts ← 同上根配置通过 glob 模式自动发现各包下的测试文件,子包只需在 __tests__/ 目录下新增 .test.ts 文件即可被收录,无需额外注册。
测试文件约定
packages/@core/utils/
├── src/
│ └── array.ts
└── __tests__/
└── array.test.ts ← 与 src 文件一一对应常用命令
# 运行全部测试
pnpm test
# 运行并生成覆盖率报告
pnpm test:coverage
# watch 模式(开发时使用)
pnpm test --watch已有测试覆盖包
| 包 | 覆盖内容 |
|---|---|
packages/@core/utils | 数组、对象、日期、加密、正则、存储、DOM 工具 |
packages/@core/color | OKLCH 调色板、Ant Design 色板算法 |
packages/@core/state | Jotai atom、storage registry、global store |
packages/@core/axios | 请求配置、类型守卫、选项处理 |
packages/@core/service | 请求创建、错误处理、状态共享 |
packages/@core/scheduler | 任务调度、任务中心 |
packages/hooks | loading、countdown、copy、array、store 等 hooks |
扩展方向
组件测试
packages/web/ui 的 UI 组件目前没有测试覆盖。对于有复杂交互逻辑的组件(受控/非受控切换、条件渲染、事件回调),可以用 React Testing Library 补充:
import { render, screen, fireEvent } from '@testing-library/react';
describe('Button', () => {
it('点击后应触发 onClick 回调', () => {
const onClick = vi.fn();
render(<Button onClick={onClick}>确认</Button>);
fireEvent.click(screen.getByText('确认'));
expect(onClick).toHaveBeenCalledOnce();
});
});API Mock 集成测试
MSW 已安装,可直接在 apps/admin 的集成测试中接入,拦截真实接口、模拟各种响应场景(正常、超时、鉴权失败)。
E2E 测试
引入 Playwright 后,推荐优先覆盖:
- 登录 / 登出流程
- 动态路由权限跳转
- 表格数据的增删改查操作
新包接入测试的流程
- 在包目录下创建
vitest.config.ts,继承共享配置 - 创建
vitest.setup.ts(如有需要) - 在
__tests__/下新增*.test.ts文件 - 在根
vitest.config.ts的TESTED_PACKAGES数组中加入该包路径(用于覆盖率收录) - 在根
vitest.config.ts的SETUP_FILES中注册 setup 文件(如有)