테스트 — 단위·통합·E2E·TDD
Unit·Integration·E2E 3계층 + TDD 철학 — AI가 만든 코드를 믿을 수 있게 하는 안전망.
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로는 핵심 유저 플로우만.
// ━━━ 단위 테스트 (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의 '정답지' 역할을 한다.
// ━━━ 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 커버가 아니다.
# 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`는 배포 전 관문.
TDD 사이클의 3단계를 순서대로 쓰시오.
E2E 테스트를 단위 테스트보다 많이 짜는 것이 테스트 피라미드 원칙이다.
테스트 구성의 표준 3단 패턴 AAA는 무엇의 약자인가?