testing
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseOutput-Based Testing for Pure Functions
纯函数的基于输出的测试
Test pure functions with input/output assertions. Never unit test hooks, components, or side effects.
仅通过输入/输出断言测试纯函数。切勿对hooks、组件或副作用进行单元测试。
When to Use
适用场景
- User asks to "write tests" or "add coverage"
- New business logic is being implemented
- TDD requested for new feature
- Coverage report shows untested utils/
- 用户要求“编写测试”或“添加测试覆盖率”
- 正在实现新的业务逻辑
- 要求使用TDD开发新功能
- 覆盖率报告显示工具类代码未被测试
Core Pattern
核心模式
TESTABLE (Functional Core) │ NOT UNIT TESTED (Imperative Shell)
───────────────────────────────│────────────────────────────────────
Pure functions in utils/ │ React hooks (useX)
Calculations, transformations │ Components (*.tsx)
Validators, formatters │ API calls, Firebase operations
State machines, reducers │ localStorage, Date.now(), Math.random()可测试(函数式核心) │ 不可单元测试(命令式外壳)
───────────────────────────────│────────────────────────────────────
utils/中的纯函数 │ React hooks(useX)
计算、转换逻辑 │ 组件(*.tsx)
验证器、格式化器 │ API调用、Firebase操作
状态机、reducers │ localStorage、Date.now()、Math.random()Implementation
实现方式
What to Test
测试对象
typescript
// ✅ TESTABLE: Pure function
export const calculateStreak = (dates: Date[], today: Date): number => {
// Pure transformation: dates → number
};
// Test with simple input/output
describe('calculateStreak', () => {
it('returns 0 for empty dates', () => {
expect(calculateStreak([], new Date('2024-01-15'))).toBe(0);
});
it('returns consecutive days count', () => {
const dates = [new Date('2024-01-14'), new Date('2024-01-15')];
expect(calculateStreak(dates, new Date('2024-01-15'))).toBe(2);
});
});typescript
// ✅ 可测试:纯函数
export const calculateStreak = (dates: Date[], today: Date): number => {
// 纯转换:dates → number
};
// 使用简单的输入/输出进行测试
describe('calculateStreak', () => {
it('当日期数组为空时返回0', () => {
expect(calculateStreak([], new Date('2024-01-15'))).toBe(0);
});
it('返回连续天数的计数', () => {
const dates = [new Date('2024-01-14'), new Date('2024-01-15')];
expect(calculateStreak(dates, new Date('2024-01-15'))).toBe(2);
});
});What NOT to Test
无需测试的对象
typescript
// ❌ DON'T TEST: Hook with side effects
export function useStreak(userId: string) {
const { data } = useQuery(['streak', userId], () => fetchStreak(userId));
return calculateStreak(data?.dates ?? [], new Date());
}
// ❌ DON'T TEST: Component
const StreakBadge = ({ userId }) => {
const streak = useStreak(userId);
return <Badge>{streak} days</Badge>;
};typescript
// ❌ 无需测试:带有副作用的Hook
export function useStreak(userId: string) {
const { data } = useQuery(['streak', userId], () => fetchStreak(userId));
return calculateStreak(data?.dates ?? [], new Date());
}
// ❌ 无需测试:组件
const StreakBadge = ({ userId }) => {
const streak = useStreak(userId);
return <Badge>{streak} days</Badge>;
};TDD Workflow
TDD工作流程
1. Write failing test for pure function
2. Implement pure function to pass test
3. Create thin hook/component that calls pure function
4. Skip unit tests for hook/component (E2E covers integration)1. 为纯函数编写失败的测试
2. 实现纯函数以通过测试
3. 创建调用纯函数的轻量Hook/组件
4. 跳过对Hook/组件的单元测试(端到端测试覆盖集成场景)Test File Location
测试文件位置
src/
├── feature/
│ ├── utils/
│ │ ├── calculations.ts # Pure functions
│ │ └── calculations.test.ts # Tests here
│ ├── hooks/
│ │ └── useFeature.ts # NO unit tests
│ └── components/
│ └── Feature.tsx # NO unit testssrc/
├── feature/
│ ├── utils/
│ │ ├── calculations.ts # 纯函数
│ │ └── calculations.test.ts # 测试文件在此处
│ ├── hooks/
│ │ └── useFeature.ts # 无需单元测试
│ └── components/
│ └── Feature.tsx # 无需单元测试Common Mistakes
常见错误
| Mistake | Symptom | Fix |
|---|---|---|
| Testing hooks | | Extract logic to pure function |
| Mocking time | | Inject timestamp as parameter |
| Mocking internals | | Test pure logic, not integration |
| Testing UI | | Only E2E tests for UI |
| 错误 | 表现 | 修复方案 |
|---|---|---|
| 测试hooks | 测试中使用 | 将逻辑提取为纯函数 |
| 模拟时间 | 使用 | 将时间戳作为参数传入 |
| 模拟内部依赖 | 使用 | 测试纯逻辑,而非集成场景 |
| 测试UI | 使用 | 仅通过端到端测试UI |
Red Flags
危险信号
Stop and refactor if your test file has:
- for anything except external APIs
vi.mock() - or
renderHook()from testing-libraryrender() - ,
QueryClient, orProvidersetupwrapper - More than 5 lines of test setup
如果你的测试文件出现以下情况,请停止并重构:
- 除外部API外,对其他内容使用
vi.mock() - 使用testing-library的或
renderHook()render() - 设置、
QueryClient或Providerwrapper - 测试代码的设置部分超过5行
Naming Convention
命名规范
typescript
describe('FeatureName', () => {
describe('when [condition]', () => {
it('[expected outcome]', () => {
// Arrange - Act - Assert (max 3-5 lines)
});
});
});typescript
describe('FeatureName', () => {
describe('when [condition]', () => {
it('[expected outcome]', () => {
// 准备 - 执行 - 断言(最多3-5行)
});
});
});