--- id: wiki-2026-0508-testability-architecture title: Testability Architecture category: 10_Wiki/Topics status: verified canonical_id: self aliases: [Test-Friendly Architecture, Design for Testability] duplicate_of: none source_trust_level: A confidence_score: 0.9 verification_status: applied tags: [testing, architecture, dependency-injection, seams] raw_sources: [] last_reinforced: 2026-05-10 github_commit: pending tech_stack: language: TypeScript framework: Vitest --- # Testability Architecture ## 매 한 줄 > **"매 testable design 은 testing 의 byproduct 가 아니라 cause"**. Michael Feathers 의 *Working Effectively with Legacy Code* 의 seam 개념 + Dependency Inversion 의 결합. 매 unit test 의 어려움 = 매 design problem 의 signal. ## 매 핵심 ### 매 testability dimensions (Robert Binder 1994) - **Observability**: 매 internal state 의 inspection. - **Controllability**: 매 input 의 injection. - **Isolability**: 매 unit 의 independent execution. - **Predictability**: 매 deterministic output. ### 매 seam types (Feathers) - **Object seam**: 매 polymorphism (interface + implementation swap). - **Preprocessor seam**: 매 macro / build flag. - **Link seam**: 매 link-time symbol 의 replacement. ### 매 응용 1. 매 hard-to-test code = refactor signal — 매 hidden coupling. 2. Dependency injection 의 systematic 적용. 3. Pure function core + I/O shell (functional core, imperative shell). ## 💻 패턴 ### Dependency injection ```typescript // Bad: hidden dependency, untestable class OrderService { async placeOrder(order: Order) { const result = await fetch('https://api.stripe.com/charge', { /* ... */ }); await db.query('INSERT INTO orders ...'); } } // Good: injected, testable interface PaymentGateway { charge(amount: number): Promise; } interface OrderRepo { save(order: Order): Promise; } class OrderService { constructor(private payments: PaymentGateway, private repo: OrderRepo) {} async placeOrder(order: Order) { await this.payments.charge(order.total); await this.repo.save(order); } } // Test it('saves order after payment', async () => { const payments = { charge: vi.fn().mockResolvedValue({ ok: true }) }; const repo = { save: vi.fn() }; const svc = new OrderService(payments, repo); await svc.placeOrder(makeOrder({ total: 100 })); expect(repo.save).toHaveBeenCalled(); }); ``` ### Functional core, imperative shell ```typescript // Pure core (easy to test) export function calculateInvoice(order: Order, taxRules: TaxRule[]): Invoice { const subtotal = order.items.reduce((s, i) => s + i.price * i.qty, 0); const tax = applyTaxRules(subtotal, taxRules); return { subtotal, tax, total: subtotal + tax }; } // Imperative shell (thin, integration-tested) export async function generateInvoice(orderId: string) { const order = await db.orders.findById(orderId); const rules = await db.taxRules.findActive(); const invoice = calculateInvoice(order, rules); // pure await db.invoices.save(invoice); await emailService.send(order.customerId, invoice); } ``` ### Hexagonal port for time ```typescript interface Clock { now(): Date; } class SystemClock implements Clock { now() { return new Date(); } } class FakeClock implements Clock { constructor(private fixed: Date) {} now() { return this.fixed; } advance(ms: number) { this.fixed = new Date(this.fixed.getTime() + ms); } } class TokenService { constructor(private clock: Clock) {} isExpired(token: { expiresAt: Date }) { return this.clock.now() > token.expiresAt; } } ``` ### Test data builder ```typescript class OrderBuilder { private order: Order = { id: 'o1', items: [], total: 0, status: 'pending' }; withItem(item: Item) { this.order.items.push(item); return this; } withStatus(s: OrderStatus) { this.order.status = s; return this; } build() { return { ...this.order }; } } // Usage const order = new OrderBuilder() .withItem({ sku: 'A', price: 10, qty: 2 }) .withStatus('paid') .build(); ``` ### Sprout method (Feathers — adding to legacy) ```typescript // Legacy untestable function processBatch(records: Record[]) { for (const r of records) { // ... 200 lines of mess ... db.update(r); } } // Add new logic via sprout (pure, testable) export function shouldArchive(record: Record, today: Date): boolean { return today.getTime() - record.createdAt.getTime() > 365 * 86400_000; } function processBatch(records: Record[]) { const today = new Date(); for (const r of records) { if (shouldArchive(r, today)) { /* new branch */ } // ... existing mess unchanged ... } } ``` ### Humble object pattern ```typescript // View has no logic — humble class CartView { render(model: CartViewModel) { /* pure rendering */ } } // Presenter has all logic — testable without DOM class CartPresenter { buildViewModel(cart: Cart): CartViewModel { /* pure */ } } ``` ## 매 결정 기준 | 상황 | Approach | |---|---| | Side-effect heavy code | Inject dependencies | | Time-sensitive logic | Clock port | | Complex object graphs in tests | Test data builder | | Legacy untouchable | Sprout method/class | | UI logic | Humble object | **기본값**: 매 constructor injection + 매 functional core + 매 ports for I/O. ## 🔗 Graph - 부모: [[Technical-Architecture]] · [[Test-Driven_Development]] - 변형: [[Hexagonal Architecture]] · [[Clean Architecture]] - 응용: [[Dependency Injection]] · [[Test Doubles (테스트 대역)]] - Adjacent: [[Test Automation Pyramid]] · [[The Two Hats]] ## 🤖 LLM 활용 **언제**: refactor for testability, seam identification, DI introduction. **언제 X**: 매 framework-specific DI container choice — 매 ecosystem convention 의 우선. ## ❌ 안티패턴 - **Mock everything**: 매 mock soup — 매 test 가 implementation 의 mirror. - **Static singletons**: untestable — 매 global state 의 source. - **`new` in business logic**: hidden dependency. - **Test private methods**: 매 leak — public surface 만 test. ## 🧪 검증 / 중복 - Verified (Feathers 2004 *WELC*; Binder 1994 testability metrics). - 신뢰도 A. ## 🕓 Changelog | 날짜 | 변경 | |---|---| | 2026-05-08 | Phase 1 | | 2026-05-10 | Manual cleanup — seams + DI + functional core patterns |