--- id: quality-test-strategy title: Test Strategy — pyramid / trophy / ice cream category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [quality, testing, vibe-coding] tech_stack: { language: "any", applicable_to: ["Engineering"] } applied_in: [] aliases: [test pyramid, test trophy, ice cream cone, integration heavy, Kent Dodds] --- # Test Strategy > "어떤 test 가 어느 비율?" **Pyramid (옛), Trophy (modern), Ice cream cone (anti)**. ## 📖 핵심 개념 - Unit: 빠름, 격리. - Integration: 의존 포함. - E2E: 사용자 flow. - 매 type 의 trade-off. ## 💻 코드 패턴 ### Test Pyramid (Mike Cohn) ``` /\ E2E (작음) / \ Integration /____\ / \ / Unit \ (큰) /__________\ ``` → 70% unit, 20% integration, 10% E2E. ### Test Trophy (Kent C. Dodds) ``` ___ / \ E2E (작음) /_____\ / \ Integration (큰) /_________\ \ / \ Unit/ (중간) \___/ Static (lint, type) — 큰 ``` → Integration 가 큰. Static 도 비. ### Ice cream cone (anti-pattern) ``` /\ Manual (큰) /__\ E2E (큰) /____\ / \ Integration /________\ / \ / Unit (작음) \ /______________\ ``` → Manual / E2E heavy. 느린, 깨짐. ### 매 type 의 cost ``` Unit: - ms - 매 commit - 빠른 feedback - Low confidence (mock heavy) Integration: - sec - 매 PR - Real DB / external - Higher confidence E2E: - min - 매 deploy - Full UI / network - Highest confidence + brittle ``` ### 권장 비율 ``` Static (lint + type): 무한. Unit: 60-70% (간단 logic). Integration: 20-30% (DB / API). E2E: 5-10% (critical path). ``` ### Static ```bash eslint src/ tsc --noEmit prettier --check ``` → "Test 의 99% 가 catch 가능". ### Unit (격리) ```ts test('add', () => { expect(add(2, 3)).toBe(5); }); ``` → Mock everything. 빠름. ### Integration (real dep) ```ts test('user creation', async () => { const user = await api.createUser({ email: 'a@x' }); const found = await db.users.findOne({ id: user.id }); expect(found).toBeDefined(); }); ``` → Real DB (testcontainer). 느린. ### E2E (Playwright) ```ts test('login flow', async ({ page }) => { await page.goto('/login'); await page.fill('[name=email]', 'a@x'); await page.fill('[name=password]', 'pw'); await page.click('button[type=submit]'); await expect(page).toHaveURL('/dashboard'); }); ``` → Real browser. 매우 느린. ### Mock 의 함정 ```ts // ❌ Mock 가 진실 안 반영 const mockDB = { findUser: jest.fn().mockResolvedValue({ id: 1 }) }; // → Real DB 가 다른 schema 면 silent break. // ✅ Integration test (real DB). ``` → Trophy 가 integration heavy 의 이유. ### Test Doubles ``` Stub: 가짜 (return value). Mock: 호출 검증. Spy: 옛 호출 record. Fake: 작은 진짜 구현 (in-memory DB). → Fake > Mock 가 일반. ``` ### Boundary test ``` - Validation: invalid input. - Auth: unauthorized. - Rate limit. - Concurrency. - Failure (network, DB). → Happy path 만 = 부족. ``` ### Coverage ``` 80% line coverage = baseline. 100% = 가능 가 brittle. 60% + critical 100% = 합리. ``` → Mutation testing 가 진짜 quality. → [[Testing_Stryker_Mutation]]. ### Snapshot test ```ts expect(component).toMatchSnapshot(); ``` → Visual regression. 주의: 매 변경 = 다른 snapshot. Update 자주. ### Contract test ```ts // Pact / Spectral // API consumer + provider 의 schema. ``` → Microservice 사이. → [[Testing_Pact_Contract_Deep]]. ### Property test ```ts import fc from 'fast-check'; test('add commutative', () => { fc.assert(fc.property(fc.integer(), fc.integer(), (a, b) => { expect(add(a, b)).toBe(add(b, a)); })); }); ``` → 100+ random input. Edge case 가 더. ### Visual regression ```ts // Chromatic / Percy // 매 component 의 screenshot 비교. ``` → UI 의 unintended change. ### Test Data ``` Factory + faker. Per-test reset. Anonymized prod data. ``` → [[Testing_Test_Data_Management]]. ### CI 의 test ```yaml - run: npm run lint - run: npm run typecheck - run: npm test # unit + integration - run: npm run e2e # Playwright ``` → 매 PR 가 빠른 unit. E2E 가 매일 / 매 deploy. ### Test 가 안 좋은 패턴 ``` - 모든 거 E2E: 느린, 깨짐. - 모든 거 mock: false confidence. - 매 line 1 test: brittle. - 변경 마다 snapshot update: 의미 X. - "Coverage 100%" 강제: 가짜 test 추가. ``` ### Test pyramid 의 이유 ``` - Unit 가 가장 빠름 + 정확. - E2E 가 가장 느린 + 깨짐. → 대부분 = unit. 작은 = E2E. ``` ### Trophy 의 이유 (Kent) ``` - Mock 가 confidence ↓. - Integration 가 진짜 동작. → Integration heavy. ``` ### React Testing Library ```ts import { render, screen } from '@testing-library/react'; test('button click', () => { render(); const btn = screen.getByRole('button'); fireEvent.click(btn); expect(screen.getByText('1')).toBeInTheDocument(); }); ``` → User-centric (a11y query). ### Test code 의 quality ``` - DRY (helper, factory). - 명확 이름 ("user can login"). - Setup / teardown 격리. - Fast (slow = skip in CI). - Stable (flaky 0%). ``` ### Flaky test ``` 간헐 fail = trust ↓. - Race condition. - Network. - Time. → Find + fix. Don't `it.skip` permanently. ``` ### Test 의 ROI ``` high: critical path E2E, complex logic unit. low: trivial getter, mock heavy. → "이 test 가 fail 시 무엇 catch?" ``` ### A/B feature test ``` 새 feature 가 release 가 있어: - Unit: feature flag 의 양쪽. - Integration: 양쪽 path. - E2E: 1 path 만 (cost). ``` ## 🤔 의사결정 기준 | 상황 | 추천 | |---|---| | 일반 React | Trophy | | Backend API | Pyramid 변형 (integration heavy) | | Library | Pyramid + property | | Critical path | E2E | | 단순 logic | Unit + property | | API contract | Pact | | UI regression | Chromatic / Percy | | Mutation | Stryker | ## ❌ 안티패턴 - **Ice cream cone**: 느린, brittle. - **Mock 모든 것**: false confidence. - **No static check**: 가장 cheap 만. - **Coverage 100% 강제**: 가짜 test. - **Flaky 무시**: trust ↓. - **E2E 만**: 매 commit slow. - **Test 만 + no E2E**: production 가 처음 진실. ## 🤖 LLM 활용 힌트 - Trophy 가 modern (integration heavy). - Static (lint + type) = 가장 cheap 의 catch. - Mutation 가 진짜 quality. - Critical path E2E 만. ## 🔗 관련 문서 - [[Testing_Test_Pyramid]] - [[Testing_Stryker_Mutation]] - [[Testing_Property_Based]]