C.W.K.
Stream
Lesson 01 of 02 · published

Unit vs E2E 예산 — 의도적으로 결정

~12 min · epilogue, budget, philosophy

Level 0테스트 호기심
0 XP0/32 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"'테스트 충분해?' 물지 마. '이거 테스트 값어치해?' 물어. 케이스당 하나."

실제로 작동하는 frame

'테스트 충분해?' 는 답 불가. 어딘가에서 ONE 테스트 더 있었으면 잡혔을 regression 이 항상 있어. 모든 미팅과 코드 리뷰에서 작동하는 frame 은 per-thing: 이거 테스트 값어치해? 두 요인:

  • Cost of failure — 이게 깨지면 얼마 들어? 너의 시간? 사용자의 시간? 돈? 신뢰? 대략 정량화.
  • Cost of the test — 짜는 데 얼마 걸려? 돌리는 데 얼마 걸려? 얼마나 자주 업데이트해야 해?

높은 cost-of-failure + 낮은 cost-of-test = 명백한 yes. 낮은 + 높은 = 명백한 no. 중간 모든 거 판단 — 명시적 framing 이 판단을 방어 가능하게 만들어.

Unit / Integration / E2E 스펙트럼, 다시 가격 매김

각 레이어가 다른 비용 특성:

  • Unit (Vitest) — 짜기 쌈 (~5분), 돌리기 쌈 (~30ms), 깨지면 좁은 blast radius (함수 하나). 최적: 순수 로직, edge case, '입력 X 가 출력 Y 산출' 표현 가능한 거.
  • Integration (Vitest + MSW + RTL) — 중간 비용 (짜는 데 ~15분, 돌리는 데 ~500ms), 중간 blast radius. 최적: mock 된 API 와 컴포넌트 인터랙션, 라우트 핸들러, multi-step 흐름.
  • E2E (Playwright) — 비쌈 (안정적인 거 짜는 데 ~30분, 돌리는 데 ~5초, 유지 brittle), 넓은 blast radius. 최적: critical 사용자 여정 — 가입, 결제, 결제, 레이어 사이 seam 깨지면 진짜 돈이나 신뢰 비용 드는 거.

'이거 테스트 하지 마' 케이스

똑같이 중요: 뭘 테스트 하지 말지 알기. 흔한 후보:

  • 프레임워크 코드. React 가 네가 넘긴 prop 렌더했어; 이건 React 테스트, 너의 거 아냐.
  • 사소한 getter / setter. 테스트가 코드보다 길어.
  • 순수 자료구조. Const 배열은 테스트 필요 없어.
  • 장식적 스타일링. 디자이너가 padding 미세 조정할 때마다 테스트 업데이트 필요하면, 테스트가 동작 아니라 구현을 테스트하는 거.
  • 지워지고 있는 코드. 죽어가는 거에 테스트 투자할 가치 없음.
'이거 테스트해야 해?' 는 '이거 문서화해야 해?' 와 같은 답 가짐 — 보통 no, 가끔 yes, 기준 비슷. 둘 다 future-you 에게 serve. 둘 다 지속 유지 비용. 둘 다 잘못된 이유로 쓰면 실패 ('docs 있어야 해' / '테스트 있어야 해' 가 둘 다의 사형 선고).

'E2E 추가할 때' 트리거

대부분 프로젝트는 첫날 E2E 필요 없어. 이 트리거 중 하나 발동할 때 E2E 테스트 추가:

  • 실제 브라우저 테스트만 잡았을 버그가 프로덕션에 도달.
  • Critical 흐름 (auth, 결제) 가 refactor 됨 — 숨 안 참고 refactor 할 수 있게 E2E 먼저 짜.
  • 여러 팀이 같은 surface 에 ship 하고 integration regression 이 주간으로 침.
  • 제품이 유료 고객 가지고 regression 비용이 진짜 돈이 됨.

이 중 어느 것도 참 아니면 너의 테스트 예산은 unit + integration 에 더 잘 쓰여. 선제적으로 추가된 E2E 는 아무도 안 돌리는 유지 부채가 돼.

리소스로서의 테스트 예산

테스팅에 쓸 수 있는 주의력이 유한해. 예산 포함: 짜는 시간, 유지 시간, CI 분, 실패 읽을 때 정신적 부하. Suite 가 예산 터질 때까지 자라게 두는 대신 suite 를 예산에 맞게 사이즈 — 그게 시니어 엔지니어 움직임. Pyramid (또는 trophy, 뭐든) 는 예산 할당 사고 위한 TOOL. 처방이 아냐.

Code

실제로 쓸 결정 매트릭스·text
# A practical test-budget decision matrix

                              │  Low cost-of-failure  │  High cost-of-failure
──────────────────────────────┼───────────────────────┼─────────────────────────
  Low cost-of-test            │  Worth it (cheap)     │  DEFINITELY worth it
  (unit, pure function)       │  e.g. format helpers  │  e.g. money math
──────────────────────────────┼───────────────────────┼─────────────────────────
  Medium cost-of-test         │  Skip unless          │  Worth it
  (integration w/ providers)  │  the test is fun      │  e.g. auth, payment
──────────────────────────────┼───────────────────────┼─────────────────────────
  High cost-of-test           │  Definitely skip      │  Worth it for the
  (E2E, flaky, slow)          │                       │  critical paths only
                              │                       │  e.g. checkout, signup
싼 unit 테스트 — 순수 로직·typescript
// Unit-level test — cheap, fast, narrow blast radius (5 min to write)
import { describe, it, expect } from 'vitest';
import { calculateTax } from './tax';

describe('calculateTax', () => {
  it('applies the standard rate', () => {
    expect(calculateTax(100, 'US-CA')).toBeCloseTo(8.75);
  });

  it('exempts food in NY', () => {
    expect(calculateTax(100, 'US-NY', { category: 'food' })).toBe(0);
  });
});
Integration 테스트 — 컴포넌트 + mock 된 API·typescript
// Integration test — moderate cost, covers a flow (~15 min to write)
import { describe, it, expect } from 'vitest';
import { render, screen } from './test-utils';
import userEvent from '@testing-library/user-event';
import { CheckoutForm } from './checkout-form';

describe('<CheckoutForm />', () => {
  it('submits with the right payload when the user fills it out', async () => {
    const user = userEvent.setup();
    render(<CheckoutForm />);

    await user.type(screen.getByLabelText('Card number'), '4242424242424242');
    await user.type(screen.getByLabelText('Expiry'), '12/30');
    await user.click(screen.getByRole('button', { name: 'Pay' }));

    expect(
      await screen.findByRole('alert', { name: /payment processing/i })
    ).toBeInTheDocument();
  });
});
E2E 테스트 — critical 여정, end-to-end·typescript
// E2E test — expensive, covers a critical journey end-to-end (~30 min stable)
import { test, expect } from '@playwright/test';

test('user can sign up, log in, and reach the dashboard', async ({ page }) => {
  // Sign up
  await page.goto('/signup');
  await page.getByLabel('Email').fill(`pippa+${Date.now()}@example.com`);
  await page.getByLabel('Password').fill('s3cret!');
  await page.getByRole('button', { name: 'Create account' }).click();

  // Land on dashboard automatically after signup
  await expect(
    page.getByRole('heading', { name: /welcome back/i })
  ).toBeVisible();

  // Sign out and sign back in — confirms the round trip
  await page.getByRole('button', { name: 'Sign out' }).click();
  await page.getByLabel('Email').fill(`pippa+${Date.now()}@example.com`);
  await page.getByLabel('Password').fill('s3cret!');
  await page.getByRole('button', { name: 'Sign in' }).click();

  await expect(
    page.getByRole('heading', { name: /welcome back/i })
  ).toBeVisible();
});

External links

Exercise

현재 테스트 suite 봐 (작으면 멘탈로 리스트). 각 테스트에 물어: 이 테스트 지우면 cost-of-failure 가 뭐고, 유지 비용은 뭐야? 식별: (a) 명확히 유지할 값어치 있는 테스트 하나, (b) 경계선 테스트 하나, (c) 진짜 신호 없는 오버헤드라고 의심되는 테스트 하나. 아직 아무것도 지우지 마 — 그냥 예산 frame 으로 보기만.
Hint
'오버헤드' 테스트는 보통 '이게 어떤 버그 잡아?' 물으면 아무도 설명 못 하는 거. 답이 '잘 모름, 아마 React 가 렌더링 멈추는 거?' 면 — 그 테스트가 너의 코드 아니라 프레임워크 테스트하는 거. 은퇴 마크.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.