115 lines
3.8 KiB
Markdown
115 lines
3.8 KiB
Markdown
---
|
|
id: testing-test-pyramid
|
|
title: Test Pyramid — 어디에 얼마나 투자할까
|
|
category: Coding
|
|
status: draft
|
|
source_trust_level: B
|
|
verification_status: conceptual
|
|
created_at: 2026-05-09
|
|
updated_at: 2026-05-09
|
|
tags: [testing, test-pyramid, vibe-coding]
|
|
tech_stack: { language: "Any", applicable_to: ["Backend", "Web", "Mobile"] }
|
|
applied_in: []
|
|
aliases: [unit, integration, e2e, testing strategy, ice cream cone]
|
|
---
|
|
|
|
# Test Pyramid
|
|
|
|
> **Unit (많이) → Integration (중간) → E2E (적게)** 가 표준. 거꾸로 ice-cream cone (E2E 만 잔뜩) 은 느림 + 깨짐 + 디버깅 지옥. 단 modern 환경에서는 integration 비중이 더 큼.
|
|
|
|
## 📖 핵심 개념
|
|
- Unit: 한 함수 / 한 컴포넌트. ms 단위. 모킹 많음.
|
|
- Integration: DB / API / 다른 모듈 결합. 초 단위. 진짜 의존성.
|
|
- E2E: 브라우저 / 디바이스 전체 시나리오. 분 단위. flaky 가능.
|
|
- Contract: 다른 서비스와의 API 약속.
|
|
|
|
비율 가이드 (출발점):
|
|
- Unit: 60-70%
|
|
- Integration: 20-30%
|
|
- E2E: 5-10%
|
|
|
|
## 💻 코드 패턴
|
|
|
|
### Unit (Jest / Vitest)
|
|
```ts
|
|
import { applyDiscount } from './pricing';
|
|
|
|
test('10% off above $100', () => {
|
|
expect(applyDiscount({ total: 150 })).toEqual({ total: 150, discount: 15 });
|
|
expect(applyDiscount({ total: 50 })).toEqual({ total: 50, discount: 0 });
|
|
});
|
|
```
|
|
|
|
### Integration — 진짜 DB
|
|
```ts
|
|
import { test, beforeAll } from 'vitest';
|
|
import { Pool } from 'pg';
|
|
|
|
const pool = new Pool({ connectionString: process.env.TEST_DATABASE_URL });
|
|
|
|
beforeAll(async () => {
|
|
await pool.query('CREATE TEMP TABLE users (id SERIAL, email TEXT UNIQUE)');
|
|
});
|
|
|
|
test('createUser inserts row', async () => {
|
|
const repo = new UserRepo(pool);
|
|
const u = await repo.create({ email: 'a@a.com' });
|
|
const found = await repo.find(u.id);
|
|
expect(found?.email).toBe('a@a.com');
|
|
});
|
|
```
|
|
testcontainers / Docker compose 로 진짜 Postgres 띄우는 게 표준.
|
|
|
|
### E2E (Playwright)
|
|
```ts
|
|
import { test, expect } from '@playwright/test';
|
|
|
|
test('login flow', async ({ page }) => {
|
|
await page.goto('/login');
|
|
await page.getByLabel('Email').fill('a@a.com');
|
|
await page.getByLabel('Password').fill('secret');
|
|
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
await expect(page).toHaveURL('/dashboard');
|
|
});
|
|
```
|
|
|
|
### Component test — 중간 zone
|
|
```ts
|
|
// React Testing Library
|
|
test('counter increments', async () => {
|
|
render(<Counter />);
|
|
await userEvent.click(screen.getByRole('button'));
|
|
expect(screen.getByText('1')).toBeInTheDocument();
|
|
});
|
|
```
|
|
|
|
## 🤔 의사결정 기준
|
|
| 코드 | 어디서 테스트 |
|
|
|---|---|
|
|
| 순수 함수 (계산, 포매팅) | Unit |
|
|
| Repository / DAO | Integration (testcontainers) |
|
|
| API endpoint | Integration (supertest) |
|
|
| React 컴포넌트 (단일) | Component (RTL + jsdom) |
|
|
| 복잡 form / 인증 / 결제 흐름 | E2E (Playwright) |
|
|
| 다른 서비스 약속 | Contract test (Pact) |
|
|
| Performance | benchmark 별도 |
|
|
|
|
## ❌ 안티패턴
|
|
- **모든 것 E2E**: 느림 + flaky. 빠른 피드백 못 받음.
|
|
- **unit test 가 모킹 천국**: 모킹된 인터페이스는 실제와 다를 수 있음 → 통합 시 사고. integration 도 필수.
|
|
- **DB 모킹 (Jest mock)**: 실제 SQL 검증 안 됨. testcontainers 권장.
|
|
- **`it.skip` 누적**: dead test. 청소.
|
|
- **flaky test 무시**: 한 번 무시하면 영구 무시. 즉시 fix 또는 quarantine.
|
|
- **테스트가 production 환경 의존**: 다른 dev 가 못 돌림. self-contained.
|
|
- **snapshot test 무지성 update**: 의미 없어짐. diff 검토 후만 update.
|
|
- **coverage 100% 강제**: 의미 없는 테스트 양산. critical path 우선.
|
|
|
|
## 🤖 LLM 활용 힌트
|
|
- 새 함수: unit test + edge case 같이 생성 요청.
|
|
- API: supertest + testcontainers 패턴.
|
|
- E2E 는 critical journey 만.
|
|
|
|
## 🔗 관련 문서
|
|
- [[Testing_Contract_Testing]]
|
|
- [[Testing_Mocking_Boundaries]]
|