关于 testing
面向软件开发者的通用测试技能,提供 TDD、测试分层、模拟和覆盖率指导。内容适用于 pytest、vitest、jest 等框架,强调验证行为并保持测试可靠、易维护。
主要功能
- 01TDD 红绿重构流程
- 02单元、集成与端到端测试分层
- 03外部依赖模拟策略
- 04测试组织与命名建议
- 05覆盖率目标和完成检查清单
- 06pytest、vitest 与 jest 示例
适用场景
- 01为新功能先编写失败测试,再逐步实现
- 02规划单元、集成和端到端测试的比例
- 03为 API、数据库等外部依赖设计模拟
- 04检查测试覆盖率、独立性与稳定性
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ scientiacapital/ skills --skill testing安装后在 Claude Code 中输入/testing确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
config.jsonreference/coverage-strategies.mdintegration-testing.mdtest-organization.mdunit-testing.md详细介绍
此技能强调编写能带来信心、又不会成为维护负担的测试。测试应快速、可靠,并聚焦于行为而非实现细节。
<quick_start> TDD 红-绿-重构循环:
-
红:先编写一个失败的测试
test('adds numbers', () => { expect(add(1, 2)).toBe(3); // Fails - add() doesn't exist }); -
绿:编写能通过测试的最少代码
const add = (a, b) => a + b; // Test passes -
重构:在测试保持通过的同时清理代码
测试金字塔:70% 单元测试、25% 集成测试、5% 端到端测试 </quick_start>
<success_criteria> 满足以下条件即表示测试成功:
- 遵循 TDD 循环:先于实现代码编写测试
- 测试金字塔均衡:约 70% 单元测试、25% 集成测试、5% 端到端测试
- 测试相互独立,可按任意顺序运行
- 没有不稳定的测试(运行 3 次以验证可靠性)
- 覆盖率达到目标:行覆盖率 70%–80%,关键路径 100%
- 测试名称描述行为(内容 + 时机 + 预期结果)
- 仅对外部依赖使用模拟,不模拟自己的代码 </success_criteria>
<core_principles>
测试思维
- 测试就是文档——失败的测试是尚未实现的规范
- 测试行为,而非实现——重构后测试仍应有效
- 快速反馈循环——单元测试应以毫秒而非秒为单位运行
- 默认隔离——每个测试都应相互独立
- 准备-执行-断言——每个测试都应有清晰的结构 </core_principles>
<tdd_workflow>
TDD:红-绿-重构
┌─────────────────────────────────────────────────────────┐
│ TDD CYCLE │
│ │
│ ┌─────────┐ │
│ │ RED │ ◄─── Write a failing test │
│ └────┬────┘ │
│ │ │
│ ▼ │
│ ┌─────────┐ │
│ │ GREEN │ ◄─── Write minimum code to pass │
│ └────┬────┘ │
│ │ │
│ ▼ │
│ ┌─────────┐ │
│ │REFACTOR │ ◄─── Clean up while tests stay green │
│ └────┬────┘ │
│ │ │
│ └──────────────► Back to RED │
└─────────────────────────────────────────────────────────┘
规则
- 先编写一个失败的测试——没有失败的测试,绝不编写生产代码
- 只编写足以让测试失败的内容——编译失败也算失败
- 只编写足以通过测试的代码——不多不少
- 只在测试通过时重构——测试失败时绝不重构
常见的 TDD 错误
| 错误 | 错误原因 | 应采取的做法 |
|---|---|---|
| 编写代码后再写测试 | 测试会沦为确认偏误 | 红-绿-重构 |
| 测试私有方法 | 测试的是实现,而非行为 | 测试公开接口 |
| 测试复杂度跨度过大 | 难以调试失败原因 | 循序渐进 |
| 跳过重构步骤 | 技术债务会不断累积 | 始终清理代码 |
| </tdd_workflow> |
<test_pyramid>
测试金字塔
┌───────────┐
│ E2E │ Few, slow, expensive
│ Tests │ (minutes)
└─────┬─────┘
│
┌──────────┴──────────┐
│ Integration Tests │ Some, medium speed
│ (API, Database) │ (seconds)
└──────────┬───────────┘
│
┌─────────────────┴─────────────────┐
│ Unit Tests │ Many, fast, cheap
│ (Functions, Components) │ (milliseconds)
└────────────────────────────────────┘
分布指南
| 类型 | 百分比 | 速度 | 范围 |
|---|---|---|---|
| 单元测试 | 70%–80% | 每个 <10ms | 单个函数/组件 |
| 集成测试 | 15%–25% | 每个 <1s | 多个组件、数据库 |
| 端到端测试 | 5%–10% | 每个 <30s | 完整用户流程 |
各类测试的测试内容
单元测试:
- 纯函数
- 业务逻辑
- 数据转换
- 验证规则
- 组件渲染
集成测试:
- API 端点
- 数据库操作
- 服务交互
- 组件集成
端到端测试:
- 关键用户流程(登录、结账)
- 仅测试正常流程
- 冒烟测试 </test_pyramid>
<when_to_mock>
模拟策略
伦敦学派与底特律学派
伦敦学派(模拟派):
- 模拟所有依赖
- 在完全隔离的环境中测试
- 测试非常聚焦
底特律学派(经典派):
- 只模拟外部服务
- 将自然形成的单元放在一起测试
- 测试更贴近实际
推荐:采用务实方法
- 模拟外部服务(API、单元测试中的数据库)
- 除非必要,否则不要模拟自己的代码
- 在集成测试中使用真实实现
应该模拟的内容
| 模拟 | 不模拟 |
|---|---|
| 外部 API | 自己的纯函数 |
| 文件系统(单元测试中) | 数据转换 |
| 网络请求 | 业务逻辑 |
| 时间/随机性 | 内存数据结构 |
| 昂贵的计算 | 简单工具函数 |
模拟模式
// GOOD: Mock external dependency
const mockFetch = vi.fn().mockResolvedValue({ data: [] });
// BAD: Mocking your own utilities
const mockFormatDate = vi.fn(); // Don't do this
// GOOD: Dependency injection for testability
function createService(httpClient = fetch) {
return {
getData: () => httpClient('/api/data')
};
}
// In test:
const mockClient = vi.fn();
const service = createService(mockClient);
</when_to_mock>
<test_structure>
测试组织
文件命名
src/
├── components/
│ ├── Button.tsx
│ └── Button.test.tsx # Colocated test
├── utils/
│ ├── format.ts
│ └── format.test.ts
└── __tests__/ # Or separate folder
└── integration/
└── api.test.ts
测试命名
// Pattern: describe what + when + expected result
describe('UserService', () => {
describe('createUser', () => {
it('returns user object when given valid email', () => {});
it('throws ValidationError when email is invalid', () => {});
it('sends welcome email after successful creation', () => {});
});
});
// Alternative: BDD style
describe('UserService', () => {
describe('when creating a user with valid data', () => {
it('should return the created user', () => {});
it('should send a welcome email', () => {});
});
describe('when email is invalid', () => {
it('should throw ValidationError', () => {});
});
});
</test_structure>
Arrange-Act-Assert
it('calculates total with discount', () => {
// Arrange - set up test data
const cart = createCart([
{ price: 100, quantity: 2 },
{ price: 50, quantity: 1 }
]);
const discount = 0.1;
// Act - perform the action
const total = calculateTotal(cart, discount);
// Assert - verify result
expect(total).toBe(225); // (200 + 50) * 0.9
});
</test_structure>
<what_not_to_test>
不要测试的内容
跳过以下内容
- 框架代码 - React 的 useState、Express 路由
- 第三方库 - 它们有自己的测试
- 简单的 getter/setter - 没有逻辑 = 无需测试
- 实现细节 - 私有方法、内部状态
- 单行函数 - 除非包含复杂逻辑
重点关注
- 业务逻辑 - Bug 藏身之处
- 边界情况 - 空值、空数组、边界
- 错误路径 - 出错时会发生什么
- 面向用户的行为 - 用户实际执行的操作
- 回归问题 - 曾经修复又再次出现的 Bug
覆盖率目标
| 指标 | 目标 | 说明 |
|---|---|---|
| 行覆盖率 | 70-80% | 越高并不总是越好 |
| 分支覆盖率 | 70-80% | 比行覆盖率更重要 |
| 关键路径 | 100% | 身份验证、支付、数据变更 |
警告: 100% 的覆盖率并不意味着测试质量好。糟糕的测试可以覆盖每一行代码,却没有测试任何有意义的内容。 </what_not_to_test>
如需了解详细模式,请加载相应参考资料:| 主题 | 参考文件 | 加载时机 |
|---|---|---|
| 单元测试模式 | reference/unit-testing.md | 编写单元测试、使用模拟 |
| 集成测试 | reference/integration-testing.md | API 测试、数据库测试 |
| 测试组织 | reference/test-organization.md | 组织测试套件 |
| 覆盖率策略 | reference/coverage-strategies.md | 设定覆盖率目标 |
加载方式: 请求特定主题,或根据上下文判断是否需要加载。
<framework_patterns>
按框架分类的快速参考
pytest(Python)
# Fixtures
@pytest.fixture
def user():
return User(name="test")
def test_user_greet(user):
assert user.greet() == "Hello, test"
# Parametrize
@pytest.mark.parametrize("input,expected", [
("hello", "HELLO"),
("world", "WORLD"),
])
def test_uppercase(input, expected):
assert uppercase(input) == expected
vitest/jest(TypeScript)
// Basic test
test('adds numbers', () => {
expect(add(1, 2)).toBe(3);
});
// Mock
vi.mock('./api', () => ({
fetchUser: vi.fn().mockResolvedValue({ name: 'test' })
}));
// Component test
import { render, screen } from '@testing-library/react';
test('renders button', () => {
render(<Button>Click</Button>);
expect(screen.getByRole('button')).toHaveTextContent('Click');
});
Testing Library 原则
- 按角色查询,而不是按测试 ID 查询
- 测试用户看到的内容,而不是实现方式
- 优先使用
userEvent,而不是fireEvent - 避免测试内部状态 </framework_patterns>
标记代码完成前:
- 单元测试覆盖正常路径
- 单元测试覆盖错误情况
- 已测试边界情况(空值、空内容、边界)
- API 端点有集成测试
- 没有不稳定的测试(运行 3 次进行验证)
- 测试彼此独立(可按任意顺序运行)
- 测试名称描述行为
- 没有硬编码的超时时间(使用 waitFor)
- 每次测试之间都会重置模拟对象
- 覆盖率符合项目标准
输出结果旁路文件
最后一步,将内容写入 ~/.claude/skill-analytics/last-outcome-testing.json:
{"ts":"[UTC ISO8601]","skill":"testing","version":"1.0.0","variant":"default",
"status":"[success|partial|error]","runtime_ms":[estimated ms from start],
"metrics":{"tests_written":[n],"coverage_delta_pct":[n]},
"error":null,"session_id":"[YYYY-MM-DD]"}
如果部分阶段失败但已生成结果,请使用状态 "partial"。仅当未生成任何输出时,才使用 "error"。