topic난이도 · 약 25

테스트 — 단위·통합·E2E·TDD

Unit·Integration·E2E 3계층 + TDD 철학 — AI가 만든 코드를 믿을 수 있게 하는 안전망.

#테스트#Unit#Integration#E2E#TDD#Vitest#Playwright
왜 배우는가

Claude가 짠 코드가 '동작함'을 어떻게 확인할까? 실행해서 눈으로 보기만 하면 엣지 케이스가 구멍투성이. 자동 테스트가 있어야 AI 리팩토링 후에도 기능이 깨지지 않는다. 바이브코딩에서 테스트는 '사치'가 아니라 '필수'다.

테스트는 '다시 확인해주는 기계'다. 한 번 짜두면 코드를 바꿀 때마다 자동으로 돌아서 "이거 망가졌어요"를 알려준다. AI가 여러 파일을 동시에 수정하는 바이브코딩에선 테스트가 있어야 과감하게 리팩토링할 수 있다.

종류무엇을 테스트?속도예시 (JS)
Unit함수 하나, 컴포넌트 하나초단위 수천 개Vitest, Jest
Integration여러 모듈·DB 포함수초~수십초Vitest + Supertest
E2E브라우저에서 실제 유저 시나리오수분Playwright, Cypress
Type check정적 타입 검증수초`tsc --noEmit`

테스트 피라미드 원칙 — 단위 테스트 많이 (80%), 통합 적당히 (15%), E2E 최소 (5%). 이유: E2E는 느리고 불안정(Flaky)하다. 단위 테스트를 많이 깔아서 빠르게 회귀 감지, E2E로는 핵심 유저 플로우만.

typescript
// ━━━ 단위 테스트 (Vitest) ━━━
import { describe, it, expect } from "vitest";
import { sum } from "./math";

describe("sum", () => {
  it("두 양수 합을 반환", () => {
    expect(sum(1, 2)).toBe(3);
  });
  it("빈 배열이면 0", () => {
    expect(sum(...[])).toBe(0);
  });
});

// ━━━ 통합 테스트 (API + DB) ━━━
import request from "supertest";
import { app } from "../app";

describe("POST /api/users", () => {
  it("유저 생성 후 조회 가능", async () => {
    const res = await request(app).post("/api/users")
      .send({ email: "a@b.com" });
    expect(res.status).toBe(201);

    const get = await request(app).get("/api/users/" + res.body.id);
    expect(get.body.email).toBe("a@b.com");
  });
});

// ━━━ E2E (Playwright) ━━━
import { test, expect } from "@playwright/test";

test("로그인 → 대시보드", async ({ page }) => {
  await page.goto("http://localhost:3000/login");
  await page.fill("[name=email]", "kim@example.com");
  await page.fill("[name=password]", "secret");
  await page.click("button[type=submit]");
  await expect(page).toHaveURL(/dashboard/);
});

세 층 한눈에. Claude에게 '이 함수의 단위 테스트 + 이 API의 통합 테스트 + 로그인 플로우의 E2E 하나 짜줘'라고 요청하면 표준 테스트가 뚝딱.

TDD(Test-Driven Development)는 "테스트를 먼저, 코드는 나중에" 철학. Red → Green → Refactor 3단계. ① 실패하는 테스트 작성(Red), ② 통과만 하는 최소 코드(Green), ③ 구조 개선(Refactor). AI와 TDD는 궁합이 좋다 — 테스트가 Claude의 '정답지' 역할을 한다.

typescript
// ━━━ TDD 사이클 예시 ━━━

// 1) Red — 실패하는 테스트부터
describe("slugify", () => {
  it("공백을 하이픈으로", () => {
    expect(slugify("Hello World")).toBe("hello-world");
  });
});
// → slugify 없음, 테스트 실패

// 2) Green — 통과만 시키는 최소 구현
export function slugify(s: string) {
  return s.toLowerCase().replace(/\s+/g, "-");
}
// → 테스트 통과

// 3) Refactor — 테스트 유지한 채 개선
export function slugify(s: string) {
  return s.trim().toLowerCase()
    .replace(/[^a-z0-9\s-]/g, "")
    .replace(/\s+/g, "-");
}
// → 테스트 여전히 통과, 코드는 더 강건

Claude에게 "이 테스트들 통과하게 slugify 구현해줘" 식으로 테스트를 먼저 주면 AI도 명확한 목표로 정확히 작업한다.

좋은 테스트나쁜 테스트
독립적 (다른 테스트 영향 X)순서 의존
결정적 (매번 같은 결과)가끔 실패 (Flaky)
빠름 (수백ms 이하)수초 걸려 개발 흐름 끊김
의도가 드러나는 이름`test1`, `foo`
Arrange-Act-Assert 3단 구성섞여서 읽기 힘듦
하나당 하나의 개념만 검증한 테스트에 여러 개

커버리지는 '테스트가 건드린 코드 비율'. 80% 넘기는 걸 목표로 하되 100% 집착은 안티패턴. 중요한 건 비즈니스 크리티컬 경로가 덮였나이지 getter 커버가 아니다.

bash
# Vitest 커버리지
npx vitest run --coverage

# Playwright 리포트
npx playwright test --reporter=html

# CI에서 실패 시 배포 중단 (GitHub Actions 예시)
# - name: Test
#   run: npm test -- --coverage --passWithNoTests

테스트 + CI 연동은 `ch09-5 관측성`, `ch25 CI/CD`에서 더 깊이 다룬다. 당장은 `npm test`가 빌드 전에 돌아야 한다는 것만.

Claude에게 테스트 요청할 때 좋은 포맷. "이 함수의 단위 테스트를 Vitest로. 케이스: ① 정상, ② 빈 입력, ③ null/undefined, ④ 경계값. AAA 패턴으로, 각 it은 한 가지만 검증." — 엣지 케이스를 명시하면 AI가 빈 곳을 채운다.

한 줄 요약: 단위는 많이, 통합은 적당히, E2E는 최소. TDD는 AI와 찰떡(테스트가 정답지). Flaky 테스트는 버그보다 해롭다. `npm test`는 배포 전 관문.

실기 드릴 3문항
edit실기 드릴 · 단답형

TDD 사이클의 3단계를 순서대로 쓰시오.

check_circle실기 드릴 · OX

E2E 테스트를 단위 테스트보다 많이 짜는 것이 테스트 피라미드 원칙이다.

edit실기 드릴 · 단답형

테스트 구성의 표준 3단 패턴 AAA는 무엇의 약자인가?