关于 vitest
面向 JavaScript 或 TypeScript 开发者的 Vitest 测试参考技能,涵盖测试结构、断言、模拟、覆盖率、CI 和故障排查。
主要功能
- 01组织测试套件与断言
- 02模拟函数和模块
- 03编写异步测试
- 04配置覆盖率与 CI
- 05测试组织与故障排查
适用场景
- 01为应用代码编写单元测试
- 02模拟 API 模块和函数
- 03为 CI 配置覆盖率报告
MCP 相关
该技能指向 `mcp__documentation__fetch_docs`,用于获取完整的 Vitest 文档。
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ claude-dev-suite/ claude-dev-suite --skill vitest安装后在 Claude Code 中输入/vitest确认已加载。
附带文件quick-ref/coverage.mdquick-ref/mocking.md
详细介绍
Vitest 核心知识
深入了解:使用
mcp__documentation__fetch_docs,并指定技术:vitest,获取全面的文档。
不适用场景
- 端到端测试 - 浏览器端端到端测试请使用
playwright或cypress - React 组件测试 - 组件测试请将此技能与
testing-library结合使用 - Jest 迁移 - 如果项目使用 Jest,请改用
jest技能(语法相似,但并不完全相同) - API 集成测试 - 请使用特定框架的集成测试技能(如 spring-boot-integration 等)
基本测试
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { calculateTotal } from './utils';
describe('calculateTotal', () => {
it('should sum array of numbers', () => {
expect(calculateTotal([1, 2, 3])).toBe(6);
});
it('should return 0 for empty array', () => {
expect(calculateTotal([])).toBe(0);
});
});
匹配器
// Equality
expect(value).toBe(exact);
expect(value).toEqual(deepEqual);
expect(value).toMatchObject(partial);
// Truthiness
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeDefined();
// Numbers
expect(value).toBeGreaterThan(n);
expect(value).toBeCloseTo(0.3, 5);
// Strings
expect(str).toMatch(/pattern/);
expect(str).toContain('substring');
// Arrays
expect(arr).toContain(item);
expect(arr).toHaveLength(3);
// Errors
expect(() => fn()).toThrow();
expect(() => fn()).toThrowError('message');
模拟
// Mock function
const mockFn = vi.fn();
mockFn.mockReturnValue(42);
mockFn.mockResolvedValue({ data: [] });
// Mock module
vi.mock('./api', () => ({
fetchUsers: vi.fn().mockResolvedValue([])
}));
// Spy
const spy = vi.spyOn(object, 'method');
expect(spy).toHaveBeenCalledWith('arg');
// Reset
vi.clearAllMocks();
vi.resetAllMocks();
异步测试
it('should fetch data', async () => {
const data = await fetchUsers();
expect(data).toHaveLength(3);
});
it('should reject', async () => {
await expect(fetchFail()).rejects.toThrow('Error');
});
配置
// vitest.config.ts
export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
coverage: { provider: 'v8', reporter: ['text', 'html'] }
}
});
生产就绪
测试组织
// Proper test structure
describe('UserService', () => {
// Setup/teardown at appropriate level
let service: UserService;
let mockDb: MockedObject<Database>;
beforeAll(async () => {
// Expensive setup once
await setupTestDatabase();
});
beforeEach(() => {
// Reset state before each test
mockDb = vi.mocked(new Database());
service = new UserService(mockDb);
vi.clearAllMocks();
});
afterAll(async () => {
// Cleanup
await teardownTestDatabase();
});
describe('create', () => {
it('should create user with valid data', async () => {
// Arrange
const input = { name: 'John', email: '[email protected]' };
mockDb.insert.mockResolvedValue({ id: '1', ...input });
// Act
const result = await service.create(input);
// Assert
expect(result).toMatchObject(input);
expect(mockDb.insert).toHaveBeenCalledWith('users', input);
});
it('should throw on duplicate email', async () => {
mockDb.insert.mockRejectedValue(new UniqueConstraintError());
await expect(service.create(input)).rejects.toThrow('Email already exists');
});
});
});
覆盖率配置
// vitest.config.ts
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'],
exclude: [
'node_modules/',
'test/',
'**/*.d.ts',
'**/*.config.*',
'**/types/**',
],
thresholds: {
lines: 80,
functions: 80,
branches: 75,
statements: 80,
},
},
},
});
CI 配置
# GitHub Actions
- name: Run tests
run: npm run test:ci
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
fail_ci_if_error: true
// package.json scripts
{
"scripts": {
"test": "vitest",
"test:ci": "vitest run --coverage --reporter=junit --outputFile=test-results.xml",
"test:watch": "vitest --watch"
}
}
监控指标
| 指标 | 目标 |
|---|---|
| 行覆盖率 | > 80% |
| 分支覆盖率 | > 75% |
| 测试执行时间 | < 60s |
| 不稳定测试比率 | 0% |
检查清单
- Arrange-Act-Assert 模式
- 独立测试(无共享状态)
- 有意义的测试描述
- 强制执行覆盖率阈值
- CI/CD 集成
- 测试中不使用 console.log
- 测试之间重置模拟
- 正确等待异步测试完成
- 覆盖边界情况
- 测试错误路径
反模式
| 反模式 | 不良影响 | 解决方案 |
|---|---|---|
| 测试实现细节 | 重构时测试会失败 | 测试行为,而非内部实现 |
| 在测试之间共享状态 | 测试不稳定,且依赖执行顺序 | 使用 beforeEach,隔离测试设置 |
| 随意等待(setTimeout) | 测试缓慢且不可靠 | 使用 waitFor 或异步工具 |
| 不重置模拟 | 前一个测试会影响下一个测试 | 在 beforeEach 中调用 vi.clearAllMocks() |
| 测试私有方法 | 与实现紧密耦合 | 通过公共 API 进行测试 |
| 一个庞大的测试 | 难以调试失败原因 | 每个测试只做一个断言(理想情况) |
| 不测试错误情况 | 生产环境中的错误可能漏网 | 测试正常路径和错误路径 |
快速故障排除
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| "Cannot find module" | 缺少模拟设置 | 检查 vi.mock() 路径是否与导入路径一致 |
| 测试超时 | 未等待异步操作 | 确保所有异步操作都使用 await |
| "Expected 0 calls, received 1" | 模拟未清除 | 在 beforeEach 中添加 vi.clearAllMocks() |
| 测试不稳定 | 共享状态或时序问题 | 隔离测试设置,避免使用 setTimeout |
| 覆盖率未更新 | 缓存问题 | 使用 --no-cache 标志运行 |
| "TypeError: vi.fn is not a function" | 缺少导入 | 从 'vitest' 导入 { vi } |
参考文档
评论
支持 Markdown