Advanced (업데이트: 2026. 6. 2.)

Claude Code 테스트 전략: Vitest, Testing Library, Playwright, CI 실전 가이드

Claude Code로 단위, 통합, E2E, CI 테스트를 설계하고 깨지기 쉬운 테스트를 줄이는 방법.

Claude Code 테스트 전략: Vitest, Testing Library, Playwright, CI 실전 가이드

테스트 전략은 단순히 테스트 개수를 늘리는 일이 아닙니다. 어떤 동작은 빠른 단위 테스트로 지키고, 어떤 경계는 통합 테스트로 확인하며, 어떤 사용자 흐름만 E2E로 보호할지 정하는 일입니다.

Claude Code에 “테스트를 추가해 줘”라고만 요청하면 현재 구현을 그대로 고정하는 얕은 테스트, CSS 클래스에 의존하는 불안정한 선택자, 커버리지만 높고 의미는 약한 테스트가 생길 수 있습니다. 더 좋은 요청은 테스트 계층, 검증할 행동, 실행할 명령, 수정 가능한 범위를 함께 전달합니다.

이 글은 공식 Claude Code common workflows, Vitest coverage, Testing Library queries, Playwright의 locators, assertions, CI를 확인해 작성했습니다.

테스트 피라미드부터 정하기

flowchart TB
  E2E["E2E: 가입, 결제, 유료 CTA"]
  INT["통합: API, DB, 폼, 컴포넌트"]
  UNIT["단위: 계산, 검증, 권한"]
  E2E --> INT --> UNIT
계층기준 비율보호할 것도구
단위 테스트60-70%순수 로직, 검증, 권한 판단Vitest
통합 테스트20-30%컴포넌트, API, DB 경계Vitest + Testing Library
E2E 테스트5-10%가입, 결제, 수익 흐름Playwright
CI 게이트모든 PRlint, 타입, 테스트, 리포트GitHub Actions

가격 계산은 단위 테스트, 결제 버튼은 통합 테스트, 글의 CTA에서 상품 페이지로 이동하는 흐름은 E2E가 적합합니다. 모든 것을 E2E로 올리면 CI가 느려지고 실패 원인을 찾기 어려워집니다.

최소 설정

npm i -D vitest @vitest/coverage-v8 jsdom \
  @testing-library/react @testing-library/jest-dom @testing-library/user-event \
  @playwright/test
npx playwright install --with-deps
{
  "scripts": {
    "test": "vitest --run",
    "test:watch": "vitest",
    "test:coverage": "vitest --run --coverage",
    "test:e2e": "playwright test"
  }
}
// vitest.config.ts
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    environment: "jsdom",
    setupFiles: ["./test/setup.ts"],
    coverage: {
      provider: "v8",
      reporter: ["text", "html"],
      thresholds: {
        lines: 80,
        functions: 80,
        branches: 75,
        statements: 80,
      },
    },
  },
});
// test/setup.ts
import "@testing-library/jest-dom/vitest";

Vitest 문서에는 v8istanbul coverage provider가 모두 설명되어 있습니다. Node와 Chromium 기반 프로젝트라면 v8이 실무에서 가장 단순한 선택입니다.

예시1: 가격 로직 단위 테스트

// src/lib/pricing.ts
export type PriceInput = {
  unitPrice: number;
  quantity: number;
  discountRate?: number;
  taxRate?: number;
};

export function calculateTotal({
  unitPrice,
  quantity,
  discountRate = 0,
  taxRate = 0.1,
}: PriceInput): number {
  if (!Number.isInteger(quantity) || quantity < 0) {
    throw new Error("quantity must be a non-negative integer");
  }
  if (unitPrice < 0) throw new Error("unitPrice must be non-negative");
  if (discountRate < 0 || discountRate > 1) {
    throw new Error("discountRate must be between 0 and 1");
  }
  const discounted = unitPrice * quantity * (1 - discountRate);
  return Math.round(discounted * (1 + taxRate));
}
// src/lib/pricing.test.ts
import { describe, expect, it } from "vitest";
import { calculateTotal } from "./pricing";

describe("calculateTotal", () => {
  it("calculates a tax-included total", () => {
    expect(calculateTotal({ unitPrice: 1000, quantity: 2 })).toBe(2200);
  });

  it("applies discount before tax", () => {
    expect(
      calculateTotal({ unitPrice: 1000, quantity: 2, discountRate: 0.2 })
    ).toBe(1760);
  });

  it("allows zero quantity", () => {
    expect(calculateTotal({ unitPrice: 1000, quantity: 0 })).toBe(0);
  });

  it("rejects invalid inputs", () => {
    expect(() => calculateTotal({ unitPrice: 1000, quantity: -1 })).toThrow(
      "quantity must be a non-negative integer"
    );
  });
});

흔한 함정은 80% 커버리지를 맞추려고 정상 케이스만 늘리는 것입니다. 가격 계산에서 중요한 것은 수량 0, 100% 할인, 잘못된 할인율, 반올림 처리입니다.

예시2: Testing Library로 CTA 컴포넌트 검증

Testing Library는 사용자가 요소를 찾는 방식과 가까운 쿼리를 우선합니다. getByRolegetByLabelText를 먼저 쓰면 접근성과 테스트 안정성이 함께 좋아집니다.

// src/components/CheckoutButton.tsx
import { useState } from "react";

type Props = {
  stock: number;
  onCheckout: () => Promise<void>;
};

export function CheckoutButton({ stock, onCheckout }: Props) {
  const [submitting, setSubmitting] = useState(false);
  const soldOut = stock <= 0;

  async function handleClick() {
    setSubmitting(true);
    try {
      await onCheckout();
    } finally {
      setSubmitting(false);
    }
  }

  return (
    <button
      type="button"
      disabled={soldOut || submitting}
      aria-busy={submitting}
      onClick={handleClick}
    >
      {soldOut ? "Sold out" : submitting ? "Processing..." : "Buy now"}
    </button>
  );
}
// src/components/CheckoutButton.test.tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { CheckoutButton } from "./CheckoutButton";

describe("CheckoutButton", () => {
  it("calls checkout when in stock", async () => {
    const user = userEvent.setup();
    const onCheckout = vi.fn().mockResolvedValue(undefined);

    render(<CheckoutButton stock={3} onCheckout={onCheckout} />);
    await user.click(screen.getByRole("button", { name: "Buy now" }));

    expect(onCheckout).toHaveBeenCalledTimes(1);
  });

  it("prevents checkout when sold out", async () => {
    const user = userEvent.setup();
    const onCheckout = vi.fn().mockResolvedValue(undefined);

    render(<CheckoutButton stock={0} onCheckout={onCheckout} />);
    const button = screen.getByRole("button", { name: "Sold out" });

    expect(button).toBeDisabled();
    await user.click(button);
    expect(onCheckout).not.toHaveBeenCalled();
  });
});

CTA는 수익 흐름과 연결되므로 .primary-button 같은 CSS 선택자나 snapshot만으로 검증하지 않는 편이 좋습니다. 사용자가 누를 수 있는 버튼인지, 문구가 보이는지, 클릭 시 행동이 일어나는지를 확인합니다.

예시3: Playwright E2E는 작게 유지

// playwright.config.ts
import { defineConfig, devices } from "@playwright/test";

const baseURL =
  process.env.PLAYWRIGHT_TEST_BASE_URL ?? "http://127.0.0.1:5173";

export default defineConfig({
  testDir: "./e2e",
  retries: process.env.CI ? 2 : 0,
  use: { baseURL, trace: "on-first-retry" },
  projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }],
  webServer: process.env.PLAYWRIGHT_TEST_BASE_URL
    ? undefined
    : {
        command: "npm run dev -- --host 127.0.0.1",
        url: baseURL,
        reuseExistingServer: !process.env.CI,
      },
});
// e2e/article-cta.spec.ts
import { expect, test } from "@playwright/test";

test("reader can move from article CTA to products", async ({ page }) => {
  await page.goto("/ko/blog/claude-code-testing-strategies");

  await page.getByRole("link", { name: /products|templates|상품/i }).click();

  await expect(page).toHaveURL(/\/products\/?$/);
  await expect(page.getByRole("heading", { name: /products|상품/i })).toBeVisible();
});

Playwright의 웹 우선 assertion은 자동으로 재시도합니다. waitForTimeout보다 사용자가 실제로 보는 상태를 기다리는 편이 안정적입니다. role, label, 안정적인 test id를 우선하고 깊은 CSS 경로나 nth()는 피합니다.

예시4: CI 게이트 추가

# .github/workflows/test.yml
name: Test

on:
  pull_request:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v6
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm run test:coverage
      - run: npx playwright install --with-deps
      - run: npm run test:e2e
        env:
          CI: "true"
      - uses: actions/upload-artifact@v5
        if: ${{ !cancelled() }}
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

Claude Code GitHub Actions를 쓴다면 공식 문서의 GA 액션인 anthropics/claude-code-action@v1을 기준으로 삼습니다. 오래된 @beta 예시는 그대로 복사하지 마세요.

Claude Code 요청 템플릿

src/lib/pricing.ts에 Vitest 단위 테스트를 추가해 주세요.
정상 케이스, 경계값, 잘못된 입력을 분리해 주세요.
구현을 먼저 바꾸지 말고, 의심되는 버그가 있으면 먼저 목록으로 알려 주세요.
npm run test -- pricing을 실행하고 결과를 요약해 주세요.
src/components/CheckoutButton.tsx에 Testing Library 테스트를 추가해 주세요.
getByRole과 userEvent.setup()을 사용해 주세요.
CSS 선택자, snapshot 중심 검증, 구현 세부사항 검증은 피해 주세요.
재고 있음, 품절, 제출 중 상태를 확인해 주세요.
글에서 상품 페이지로 이동하는 CTA 흐름에 Playwright E2E 테스트를 1개만 추가해 주세요.
waitForTimeout, 깊은 CSS 선택자, nth()를 피해 주세요.
웹 우선 assertion을 사용하고 수익 흐름에만 집중해 주세요.
최신 CI 실패 로그를 읽어 주세요.
실패를 lint, typecheck, unit, e2e, environment 중 하나로 분류해 주세요.
가장 작은 변경으로 근본 원인을 고쳐 주세요.
테스트를 skip하거나 timeout만 늘리는 수정은 하지 마세요.

흔한 함정

모든 것을 mock하지 마세요. 결제사와 메일 발송은 mock해도 되지만 가격, 권한, 저장 로직은 적절한 계층에서 실제로 검증해야 합니다.

E2E로 모든 시각 요소를 고정하지 마세요. E2E는 독자가 가입, 구매, 상담, 상품 클릭으로 이동할 수 있는지를 보호해야 합니다.

커버리지를 품질 증명으로 보지 마세요. 80% 커버리지라도 삭제, 결제, 배포 분기가 빠져 있으면 위험합니다.

Claude Code에 정상 경로만 주지 마세요. 취약한 선택자 금지, skip 금지, snapshot-only 금지, 구현 세부사항 결합 금지를 함께 적으면 결과가 안정됩니다.

CTA

테스트 전략은 품질뿐 아니라 수익 흐름도 지킵니다. 먼저 무료 Claude Code cheatsheet로 기본 명령을 고정하고, 반복되는 리뷰 문구는 products and templates로 정리하세요. 팀에서 CI, 테스트 소유권, 리뷰 기준을 함께 세워야 한다면 Claude Code training이 현실적입니다. 관련해서 TDD with Claude Code, CI/CD setup, debugging techniques도 참고하세요.

실제로 시험해 본 결과

Masa는 ClaudeCodeLab의 글 CTA와 상품 페이지 흐름에 이 패턴을 적용해 보았습니다. 가장 효과가 컸던 것은 E2E를 많이 늘리는 일이 아니라, 가격 로직은 단위 테스트로 고정하고 CTA 동작은 Testing Library로 확인하며, Playwright는 글에서 상품 페이지로 가는 1개 흐름에 집중하는 것이었습니다. Claude Code에 먼저 실패 모드를 나열하게 하자 skip 테스트와 취약한 CSS 선택자가 줄었습니다. 남은 위험은 외부 결제와 광고 스크립트처럼 로컬에서 완전히 재현하기 어려운 부분이며, CI 리포트와 짧은 수동 릴리스 확인이 필요합니다.

#Claude Code #testing #test strategy #Vitest #Playwright
무료

무료 PDF: Claude Code 치트시트

이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.

개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.

Masa

작성자 소개

Masa

Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.