testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Output-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 tests
src/
├── feature/
│   ├── utils/
│   │   ├── calculations.ts      # 纯函数
│   │   └── calculations.test.ts # 测试文件在此处
│   ├── hooks/
│   │   └── useFeature.ts        # 无需单元测试
│   └── components/
│       └── Feature.tsx          # 无需单元测试

Common Mistakes

常见错误

MistakeSymptomFix
Testing hooks
renderHook()
,
QueryClientProvider
in test
Extract logic to pure function
Mocking time
vi.useFakeTimers()
,
mockDate
Inject timestamp as parameter
Mocking internals
vi.mock('../api/firebase')
Test pure logic, not integration
Testing UI
render()
,
screen.getByText()
Only E2E tests for UI
错误表现修复方案
测试hooks测试中使用
renderHook()
QueryClientProvider
将逻辑提取为纯函数
模拟时间使用
vi.useFakeTimers()
mockDate
将时间戳作为参数传入
模拟内部依赖使用
vi.mock('../api/firebase')
测试纯逻辑,而非集成场景
测试UI使用
render()
screen.getByText()
仅通过端到端测试UI

Red Flags

危险信号

Stop and refactor if your test file has:
  • vi.mock()
    for anything except external APIs
  • renderHook()
    or
    render()
    from testing-library
  • QueryClient
    ,
    Provider
    , or
    wrapper
    setup
  • More than 5 lines of test setup
如果你的测试文件出现以下情况,请停止并重构:
  • 除外部API外,对其他内容使用
    vi.mock()
  • 使用testing-library的
    renderHook()
    render()
  • 设置
    QueryClient
    Provider
    wrapper
  • 测试代码的设置部分超过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行)
    });
  });
});