"매 unit test 가 base, e2e 가 tip". Mike Cohn 의 2009 Succeeding with Agile 에서 origin — slow/expensive UI test 의 over-reliance 의 anti-pattern. 매 2026 modern stack 은 unit + integration + contract + e2e 의 4-layer 의 evolution.
매 핵심
매 layers (bottom-up)
Unit (70%): pure function, 단일 class, <10ms, in-memory.
Integration (20%): DB, HTTP client, 매 real adapter, 100ms-1s.
Contract (5-10%): provider/consumer 매 Pact / OpenAPI schema.
E2E (5%): 매 full user journey, Playwright/Cypress, 10s+.
매 anti-shapes
Ice cream cone: 매 e2e heavy, unit 부족 — slow CI, flaky.
Hourglass: 매 unit + e2e 만, integration 의 gap — production bug 의 source.
Cupcake: manual test layer 의 추가 — automation 의 의미 상실.
매 응용
CI pipeline tier — unit on every commit, e2e on merge to main.
Bug 발견 시 lowest-level test 의 추가 (root cause level).
Test budget 분배 결정 (시간 / 신뢰도 trade-off).
💻 패턴
Vitest unit test
import{describe,it,expect}from'vitest';import{calculateDiscount}from'./pricing';describe('calculateDiscount',()=>{it('applies 10% for VIP customer',()=>{expect(calculateDiscount(100,'VIP')).toBe(90);});it('returns full price for standard customer',()=>{expect(calculateDiscount(100,'STANDARD')).toBe(100);});});
Integration test (real Postgres via testcontainers)
import{PactV3,MatchersV3}from'@pact-foundation/pact';constprovider=newPactV3({consumer:'web',provider:'api'});provider.uponReceiving('a request for user').withRequest({method:'GET',path:'/users/1'}).willRespondWith({status: 200,body:{id: MatchersV3.string('1'),name: MatchersV3.string('Alice')}});
Playwright E2E
import{test,expect}from'@playwright/test';test('user can checkout',async({page})=>{awaitpage.goto('/products/42');awaitpage.getByRole('button',{name:'Add to cart'}).click();awaitpage.goto('/cart');awaitpage.getByRole('button',{name:'Checkout'}).click();awaitexpect(page).toHaveURL(/\/orders\/\w+/);});
Test pyramid CI distribution (GitHub Actions)
jobs:unit:runs-on:ubuntu-lateststeps:- run:pnpm test:unit # <30s, every pushintegration:needs:unitsteps:- run:pnpm test:integration # <5min, every PRe2e:needs:integrationif:github.ref == 'refs/heads/main'steps:- run:pnpm test:e2e # <20min, main only