왜 테스트 자동 생성이 중요한가?

소프트웨어 개발에서 테스트 코드는 선택이 아닌 필수입니다. 하지만 현실적으로 많은 개발팀이 테스트 작성에 충분한 시간을 투자하지 못하고 있습니다. 기능 개발 일정에 쫓기다 보면 테스트는 항상 후순위로 밀리게 되고, 결국 테스트 커버리지가 낮은 상태로 프로덕션에 배포되는 일이 반복됩니다.

테스트 코드가 부족하면 어떤 문제가 발생할까요? 먼저, 리팩토링이 두려워집니다. 기존 코드를 수정했을 때 다른 곳에서 버그가 발생할 수 있다는 불안감 때문에 코드 개선을 미루게 됩니다. 둘째, 배포 시 장애 발생률이 높아집니다. 수동 테스트로는 모든 경계 조건(edge case)을 확인하기 어렵습니다. 셋째, 새로운 팀원의 온보딩이 어려워집니다. 테스트 코드는 곧 살아있는 문서이므로, 테스트가 없으면 코드의 의도를 파악하기 힘듭니다.

Claude Code는 이 문제를 해결하는 강력한 도구입니다. 소스 코드를 분석해 적절한 테스트 케이스를 자동으로 생성하며, 경계 조건, 에러 케이스, 정상 흐름 모두를 포괄하는 테스트를 만들어줍니다. 실제로 Claude Code를 활용한 팀들은 테스트 커버리지를 평균 40% 이상 향상시켰다는 보고가 있습니다.

자동화된 테스트 코드를 작성하는 개발 환경 화면

이 가이드는 Claude Code CLI를 이미 설치하고 기본적인 사용법을 알고 있다는 전제 하에 작성되었습니다. 아직 설치하지 않았다면 스킬 라이브러리에서 시작 방법을 확인하세요.

테스트 자동 생성 전략 개요

Claude Code를 사용한 테스트 자동 생성에는 크게 5가지 접근법이 있습니다. 각 방법은 서로 다른 상황과 목적에 최적화되어 있으므로, 프로젝트의 특성과 팀의 워크플로우에 맞는 방법을 선택하는 것이 중요합니다.

방법 테스트 유형 적합한 상황 난이도 커버리지 향상
/test 명령어 단위 테스트 빠른 테스트 생성 초급 +20~30%
TDD 에이전트 워크플로우 단위/통합 테스트 새 기능 개발 중급 +40~60%
커스텀 테스트 스킬 팀 맞춤 테스트 팀 표준 적용 중급 +30~50%
MCP 통합 테스트 통합 테스트 외부 서비스 연동 고급 +25~40%
Playwright E2E E2E 테스트 UI 기능 검증 고급 +15~25%

이제 각 방법을 구체적인 코드 예제와 함께 자세히 살펴보겠습니다.

방법 1: /test 명령어로 단위 테스트 즉시 생성

가장 간단하고 빠른 방법입니다. Claude Code의 /test 명령어를 사용하면 지정한 파일이나 함수에 대한 단위 테스트를 즉시 생성할 수 있습니다. 이 방법은 기존 코드베이스에 테스트를 빠르게 추가해야 할 때 가장 효과적입니다.

기본 사용법

터미널에서 Claude Code를 실행한 후 다음과 같이 입력합니다.

# 특정 파일에 대한 테스트 생성
/test src/utils/calculator.ts

# 특정 함수에 대한 테스트 생성
/test src/utils/calculator.ts --function add

# 디렉토리 전체에 대한 테스트 생성
/test src/utils/

Jest 단위 테스트 예제

다음은 간단한 유틸리티 함수가 있다고 가정합니다.

// src/utils/calculator.ts
export function add(a: number, b: number): number {
  return a + b;
}

export function divide(a: number, b: number): number {
  if (b === 0) throw new Error('Division by zero');
  return a / b;
}

export function calculateDiscount(price: number, discountPercent: number): number {
  if (price < 0) throw new Error('Price cannot be negative');
  if (discountPercent < 0 || discountPercent > 100) {
    throw new Error('Discount must be between 0 and 100');
  }
  return price * (1 - discountPercent / 100);
}

/test src/utils/calculator.ts를 실행하면 Claude Code가 다음과 같은 포괄적인 테스트를 생성합니다.

// src/utils/__tests__/calculator.test.ts
import { add, divide, calculateDiscount } from '../calculator';

describe('calculator', () => {
  describe('add', () => {
    it('두 양수를 더한다', () => {
      expect(add(2, 3)).toBe(5);
    });

    it('음수를 더한다', () => {
      expect(add(-1, -2)).toBe(-3);
    });

    it('양수와 음수를 더한다', () => {
      expect(add(5, -3)).toBe(2);
    });

    it('0을 더한다', () => {
      expect(add(0, 5)).toBe(5);
    });

    it('소수점 숫자를 더한다', () => {
      expect(add(0.1, 0.2)).toBeCloseTo(0.3);
    });
  });

  describe('divide', () => {
    it('두 수를 나눈다', () => {
      expect(divide(10, 2)).toBe(5);
    });

    it('소수점 결과를 반환한다', () => {
      expect(divide(7, 2)).toBe(3.5);
    });

    it('0으로 나눌 때 에러를 던진다', () => {
      expect(() => divide(10, 0)).toThrow('Division by zero');
    });

    it('음수로 나눈다', () => {
      expect(divide(-10, 2)).toBe(-5);
    });
  });

  describe('calculateDiscount', () => {
    it('정상적인 할인을 계산한다', () => {
      expect(calculateDiscount(10000, 20)).toBe(8000);
    });

    it('0% 할인은 원래 가격을 반환한다', () => {
      expect(calculateDiscount(10000, 0)).toBe(10000);
    });

    it('100% 할인은 0을 반환한다', () => {
      expect(calculateDiscount(10000, 100)).toBe(0);
    });

    it('음수 가격에 에러를 던진다', () => {
      expect(() => calculateDiscount(-100, 10)).toThrow('Price cannot be negative');
    });

    it('범위를 벗어난 할인율에 에러를 던진다', () => {
      expect(() => calculateDiscount(100, 101)).toThrow('Discount must be between 0 and 100');
      expect(() => calculateDiscount(100, -1)).toThrow('Discount must be between 0 and 100');
    });
  });
});

/test 명령어는 소스 코드의 타입 정보, 에러 처리 패턴, 경계 조건을 분석하여 자동으로 테스트 케이스를 도출합니다. 특히 TypeScript 프로젝트에서 타입 정보를 활용해 더 정확한 테스트를 생성합니다.

Pytest 단위 테스트 예제

Python 프로젝트에서도 동일하게 사용할 수 있습니다.

# src/services/user_service.py
from typing import Optional
from dataclasses import dataclass

@dataclass
class User:
    id: int
    name: str
    email: str
    is_active: bool = True

class UserService:
    def __init__(self):
        self._users: dict[int, User] = {}
        self._next_id = 1

    def create_user(self, name: str, email: str) -> User:
        if not name or not name.strip():
            raise ValueError("이름은 비어있을 수 없습니다")
        if '@' not in email:
            raise ValueError("유효하지 않은 이메일 형식입니다")
        user = User(id=self._next_id, name=name.strip(), email=email.lower())
        self._users[user.id] = user
        self._next_id += 1
        return user

    def get_user(self, user_id: int) -> Optional[User]:
        return self._users.get(user_id)

    def deactivate_user(self, user_id: int) -> bool:
        user = self._users.get(user_id)
        if user is None:
            raise ValueError(f"사용자 {user_id}를 찾을 수 없습니다")
        user.is_active = False
        return True

/test src/services/user_service.py를 실행하면 다음과 같은 Pytest 테스트가 생성됩니다.

# tests/test_user_service.py
import pytest
from src.services.user_service import UserService, User

class TestUserService:
    def setup_method(self):
        self.service = UserService()

    def test_create_user_success(self):
        user = self.service.create_user("홍길동", "[email protected]")
        assert user.name == "홍길동"
        assert user.email == "[email protected]"
        assert user.is_active is True
        assert user.id == 1

    def test_create_user_strips_whitespace(self):
        user = self.service.create_user("  홍길동  ", "[email protected]")
        assert user.name == "홍길동"

    def test_create_user_lowercases_email(self):
        user = self.service.create_user("홍길동", "[email protected]")
        assert user.email == "[email protected]"

    def test_create_user_empty_name_raises(self):
        with pytest.raises(ValueError, match="이름은 비어있을 수 없습니다"):
            self.service.create_user("", "[email protected]")

    def test_create_user_whitespace_name_raises(self):
        with pytest.raises(ValueError, match="이름은 비어있을 수 없습니다"):
            self.service.create_user("   ", "[email protected]")

    def test_create_user_invalid_email_raises(self):
        with pytest.raises(ValueError, match="유효하지 않은 이메일"):
            self.service.create_user("홍길동", "invalid-email")

    def test_create_user_increments_id(self):
        user1 = self.service.create_user("홍길동", "[email protected]")
        user2 = self.service.create_user("김철수", "[email protected]")
        assert user2.id == user1.id + 1

    def test_get_user_returns_user(self):
        created = self.service.create_user("홍길동", "[email protected]")
        found = self.service.get_user(created.id)
        assert found is not None
        assert found.name == "홍길동"

    def test_get_user_not_found_returns_none(self):
        assert self.service.get_user(999) is None

    def test_deactivate_user_success(self):
        user = self.service.create_user("홍길동", "[email protected]")
        result = self.service.deactivate_user(user.id)
        assert result is True
        assert self.service.get_user(user.id).is_active is False

    def test_deactivate_nonexistent_user_raises(self):
        with pytest.raises(ValueError, match="사용자 999를 찾을 수 없습니다"):
            self.service.deactivate_user(999)

방법 2: TDD 에이전트 워크플로우

Test-Driven Development(TDD)는 테스트를 먼저 작성하고, 그 테스트를 통과하는 코드를 구현하는 개발 방법론입니다. Claude Code의 에이전트 기능을 활용하면 TDD 워크플로우를 자동화할 수 있습니다. 에이전트가 요구사항을 분석하고, 테스트를 먼저 작성한 후, 구현 코드를 생성하는 전체 과정을 수행합니다.

테스트 주도 개발 방식으로 코드를 작성하는 모니터 화면

TDD 에이전트 설정

Claude Code에서 TDD 에이전트를 설정하려면 다음과 같이 에이전트 설정 파일을 작성합니다.

# .claude/agents/tdd-agent.md
---
name: "tdd-agent"
description: "TDD 방식으로 기능을 개발합니다"
triggers:
  - "TDD"
  - "테스트 먼저"
  - "test first"
---

# TDD 에이전트

## 워크플로우

### 1단계: 요구사항 분석
- 사용자의 기능 요구사항을 분석합니다
- 핵심 동작(behavior)을 목록으로 정리합니다
- 경계 조건과 에러 케이스를 식별합니다

### 2단계: 테스트 작성 (RED)
- 식별된 동작에 대한 테스트를 작성합니다
- 테스트가 실패하는 것을 확인합니다
- 테스트 이름은 행동을 명확히 설명해야 합니다

### 3단계: 구현 (GREEN)
- 테스트를 통과하는 최소한의 코드를 구현합니다
- 모든 테스트가 통과하는지 확인합니다

### 4단계: 리팩토링 (REFACTOR)
- 코드 품질을 개선합니다
- 테스트가 여전히 통과하는지 확인합니다

## 규칙
- 테스트 없이 코드를 작성하지 않습니다
- 각 단계의 결과를 사용자에게 보고합니다
- 테스트 커버리지 80% 이상을 목표로 합니다

TDD 에이전트 실행 예제

다음과 같이 TDD 에이전트를 실행할 수 있습니다.

# Claude Code에서 TDD 에이전트 실행
> TDD로 장바구니 서비스를 만들어줘.
> - 상품 추가/삭제
> - 수량 변경
> - 총 금액 계산 (할인 적용)
> - 재고 확인

에이전트가 먼저 생성하는 테스트 코드입니다.

// tests/cart.test.ts
import { CartService, CartItem, Product } from '../src/services/cart';

describe('CartService', () => {
  let cart: CartService;
  const sampleProduct: Product = {
    id: 'prod-1',
    name: '노트북',
    price: 1500000,
    stock: 10,
  };

  beforeEach(() => {
    cart = new CartService();
  });

  describe('상품 추가', () => {
    it('장바구니에 상품을 추가한다', () => {
      cart.addItem(sampleProduct, 1);
      expect(cart.getItems()).toHaveLength(1);
      expect(cart.getItems()[0].product.id).toBe('prod-1');
    });

    it('같은 상품을 추가하면 수량이 증가한다', () => {
      cart.addItem(sampleProduct, 1);
      cart.addItem(sampleProduct, 2);
      expect(cart.getItems()).toHaveLength(1);
      expect(cart.getItems()[0].quantity).toBe(3);
    });

    it('재고보다 많이 추가하면 에러를 던진다', () => {
      expect(() => cart.addItem(sampleProduct, 11)).toThrow('재고 부족');
    });

    it('수량이 0 이하이면 에러를 던진다', () => {
      expect(() => cart.addItem(sampleProduct, 0)).toThrow('수량은 1 이상');
    });
  });

  describe('상품 삭제', () => {
    it('장바구니에서 상품을 삭제한다', () => {
      cart.addItem(sampleProduct, 1);
      cart.removeItem('prod-1');
      expect(cart.getItems()).toHaveLength(0);
    });

    it('존재하지 않는 상품 삭제 시 에러를 던진다', () => {
      expect(() => cart.removeItem('nonexistent')).toThrow('상품을 찾을 수 없습니다');
    });
  });

  describe('수량 변경', () => {
    it('상품 수량을 변경한다', () => {
      cart.addItem(sampleProduct, 1);
      cart.updateQuantity('prod-1', 5);
      expect(cart.getItems()[0].quantity).toBe(5);
    });

    it('재고 초과 수량 변경 시 에러를 던진다', () => {
      cart.addItem(sampleProduct, 1);
      expect(() => cart.updateQuantity('prod-1', 11)).toThrow('재고 부족');
    });
  });

  describe('총 금액 계산', () => {
    it('상품 가격 * 수량의 합을 반환한다', () => {
      cart.addItem(sampleProduct, 2);
      expect(cart.getTotal()).toBe(3000000);
    });

    it('할인을 적용한 금액을 반환한다', () => {
      cart.addItem(sampleProduct, 2);
      cart.applyDiscount(10); // 10% 할인
      expect(cart.getTotal()).toBe(2700000);
    });

    it('빈 장바구니는 0을 반환한다', () => {
      expect(cart.getTotal()).toBe(0);
    });
  });
});

테스트를 먼저 작성한 후, 에이전트가 이 테스트를 통과하는 구현 코드를 자동으로 생성합니다. 이 과정에서 에이전트는 각 단계(RED-GREEN-REFACTOR)의 결과를 보고하며, 최종적으로 모든 테스트가 통과하는 것을 확인해줍니다.

TDD 에이전트는 테스트가 실패하는 것을 먼저 확인(RED 단계)한 후 구현을 진행합니다. 이를 통해 테스트가 실제로 유의미한 검증을 수행하는지 보장할 수 있습니다.

방법 3: 커스텀 테스트 생성 스킬

팀마다 테스트 작성 규칙이 다릅니다. 네이밍 컨벤션, 테스트 구조, 목(mock) 전략 등이 팀의 코딩 표준에 따라 달라집니다. 커스텀 스킬을 만들면 팀의 테스트 표준을 Claude Code에 내장시킬 수 있습니다.

테스트 생성 스킬 작성

# .claude/skills/test-generator/SKILL.md
---
name: "team-test-generator"
description: "팀 표준에 맞는 테스트를 생성합니다"
triggers:
  - "테스트 생성"
  - "테스트 만들어"
  - "generate test"
version: "2.0.0"
author: "DevTeam"
---

# 팀 테스트 생성 스킬

## 테스트 네이밍 규칙
- describe: 테스트 대상 클래스/함수명 (한글 금지)
- it/test: 한글로 행동을 설명 (예: "유효하지 않은 이메일에 에러를 반환한다")
- 접두사 금지: "should", "test_" 등 사용하지 않음

## 필수 테스트 카테고리
모든 함수/메서드에 대해 다음 카테고리의 테스트를 반드시 포함:
1. **정상 케이스**: 기대되는 입력에 대한 정상 동작
2. **경계 케이스**: 빈 값, 최대값, 최소값, null, undefined
3. **에러 케이스**: 잘못된 입력, 예외 상황
4. **비동기 케이스**: Promise, async/await 관련 (해당 시)

## Mock 전략
- 외부 API 호출: 반드시 Mock
- 데이터베이스: In-memory DB 또는 Mock
- 파일 시스템: Mock 또는 임시 디렉토리
- 시간 의존 로직: jest.useFakeTimers()

## 테스트 파일 구조
```
src/
  services/
    user.service.ts
  __tests__/
    user.service.test.ts
```

## Arrange-Act-Assert 패턴 필수
모든 테스트는 AAA 패턴을 따릅니다:
```typescript
it('설명', () => {
  // Arrange: 테스트 준비
  const input = createTestInput();

  // Act: 실행
  const result = functionUnderTest(input);

  // Assert: 검증
  expect(result).toBe(expected);
});
```

커스텀 스킬 적용 결과

이 스킬이 활성화된 상태에서 테스트를 생성하면 팀의 규칙이 자동으로 적용됩니다. 예를 들어 API 클라이언트에 대한 테스트를 생성하면 다음과 같습니다.

// src/__tests__/api-client.test.ts
import { ApiClient } from '../services/api-client';
import axios from 'axios';

jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;

describe('ApiClient', () => {
  let client: ApiClient;

  beforeEach(() => {
    client = new ApiClient('https://api.example.com');
    jest.clearAllMocks();
  });

  describe('get', () => {
    it('성공 응답을 반환한다', async () => {
      // Arrange
      const mockData = { id: 1, name: '테스트' };
      mockedAxios.get.mockResolvedValue({ data: mockData, status: 200 });

      // Act
      const result = await client.get('/users/1');

      // Assert
      expect(result).toEqual(mockData);
      expect(mockedAxios.get).toHaveBeenCalledWith(
        'https://api.example.com/users/1',
        expect.any(Object)
      );
    });

    it('404 응답에 null을 반환한다', async () => {
      // Arrange
      mockedAxios.get.mockRejectedValue({ response: { status: 404 } });

      // Act
      const result = await client.get('/users/999');

      // Assert
      expect(result).toBeNull();
    });

    it('네트워크 에러에 재시도한다', async () => {
      // Arrange
      mockedAxios.get
        .mockRejectedValueOnce(new Error('Network Error'))
        .mockResolvedValue({ data: { id: 1 }, status: 200 });

      // Act
      const result = await client.get('/users/1');

      // Assert
      expect(result).toEqual({ id: 1 });
      expect(mockedAxios.get).toHaveBeenCalledTimes(2);
    });

    it('빈 문자열 경로에 에러를 던진다', async () => {
      // Act & Assert
      await expect(client.get('')).rejects.toThrow('경로는 비어있을 수 없습니다');
    });
  });
});

방법 4: MCP 연동 통합 테스트

실제 프로젝트에서는 단위 테스트만으로는 충분하지 않습니다. 여러 서비스가 상호작용하는 통합 테스트가 필요합니다. Claude Code의 MCP(Model Context Protocol) 기능을 활용하면 데이터베이스, 외부 API, 메시지 큐 등 외부 시스템과의 연동 테스트를 자동으로 생성할 수 있습니다.

MCP를 활용한 데이터베이스 통합 테스트

먼저 MCP 서버 설정에 데이터베이스 연결 정보를 추가합니다.

// .claude/mcp.json
{
  "mcpServers": {
    "postgres": {
      "command": "mcp-server-postgres",
      "args": ["--connection-string", "postgresql://test:test@localhost:5432/testdb"],
      "env": {
        "NODE_ENV": "test"
      }
    }
  }
}

MCP를 통해 Claude Code가 실제 데이터베이스 스키마를 읽고, 그에 맞는 통합 테스트를 생성합니다.

# tests/integration/test_order_service.py
import pytest
import asyncpg
from src.services.order_service import OrderService
from src.models.order import Order, OrderItem

@pytest.fixture
async def db_pool():
    pool = await asyncpg.create_pool(
        'postgresql://test:test@localhost:5432/testdb'
    )
    # 테스트 전 데이터 초기화
    async with pool.acquire() as conn:
        await conn.execute('DELETE FROM order_items')
        await conn.execute('DELETE FROM orders')
        await conn.execute('DELETE FROM products')
        # 테스트 데이터 삽입
        await conn.execute("""
            INSERT INTO products (id, name, price, stock)
            VALUES (1, '노트북', 1500000, 10),
                   (2, '마우스', 35000, 100)
        """)
    yield pool
    await pool.close()

@pytest.fixture
async def order_service(db_pool):
    return OrderService(db_pool)

class TestOrderServiceIntegration:
    @pytest.mark.asyncio
    async def test_create_order_with_items(self, order_service, db_pool):
        # Arrange
        items = [
            OrderItem(product_id=1, quantity=2),
            OrderItem(product_id=2, quantity=3),
        ]

        # Act
        order = await order_service.create_order(
            user_id=1, items=items
        )

        # Assert
        assert order.id is not None
        assert order.total_amount == 3105000  # 1500000*2 + 35000*3
        assert len(order.items) == 2

        # DB에서 직접 확인
        async with db_pool.acquire() as conn:
            row = await conn.fetchrow(
                'SELECT * FROM orders WHERE id = $1', order.id
            )
            assert row['total_amount'] == 3105000

    @pytest.mark.asyncio
    async def test_create_order_reduces_stock(self, order_service, db_pool):
        # Arrange
        items = [OrderItem(product_id=1, quantity=2)]

        # Act
        await order_service.create_order(user_id=1, items=items)

        # Assert
        async with db_pool.acquire() as conn:
            stock = await conn.fetchval(
                'SELECT stock FROM products WHERE id = 1'
            )
            assert stock == 8  # 10 - 2

    @pytest.mark.asyncio
    async def test_create_order_insufficient_stock(self, order_service):
        # Arrange
        items = [OrderItem(product_id=1, quantity=11)]

        # Act & Assert
        with pytest.raises(ValueError, match="재고 부족"):
            await order_service.create_order(user_id=1, items=items)

    @pytest.mark.asyncio
    async def test_cancel_order_restores_stock(self, order_service, db_pool):
        # Arrange
        items = [OrderItem(product_id=1, quantity=3)]
        order = await order_service.create_order(user_id=1, items=items)

        # Act
        await order_service.cancel_order(order.id)

        # Assert
        async with db_pool.acquire() as conn:
            stock = await conn.fetchval(
                'SELECT stock FROM products WHERE id = 1'
            )
            assert stock == 10  # 재고 복원

            order_row = await conn.fetchrow(
                'SELECT * FROM orders WHERE id = $1', order.id
            )
            assert order_row['status'] == 'cancelled'

MCP 연동 테스트를 실행하기 전에 Docker Compose로 테스트용 데이터베이스를 준비하는 것을 권장합니다. docker-compose -f docker-compose.test.yml up -d로 테스트 환경을 빠르게 구성할 수 있습니다.

방법 5: Playwright를 활용한 E2E 테스트 생성

E2E(End-to-End) 테스트는 실제 사용자의 관점에서 애플리케이션의 전체 흐름을 검증합니다. Claude Code는 페이지의 HTML 구조를 분석하고 사용자 시나리오에 맞는 Playwright 테스트를 자동으로 생성할 수 있습니다.

노트북에서 E2E 테스트 코드를 작성하는 개발자의 작업 환경

E2E 테스트 생성 프롬프트

# Claude Code에서 E2E 테스트 요청
> 로그인 페이지에 대한 E2E 테스트를 Playwright로 생성해줘.
> 페이지 URL: /login
> 테스트 시나리오:
> 1. 정상 로그인 후 대시보드 이동
> 2. 잘못된 비밀번호 에러 메시지
> 3. 빈 필드 유효성 검사
> 4. 소셜 로그인 버튼 동작
> 5. 비밀번호 찾기 링크 동작

생성된 Playwright E2E 테스트

// e2e/login.spec.ts
import { test, expect } from '@playwright/test';

test.describe('로그인 페이지', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/login');
  });

  test('정상적인 이메일/비밀번호로 로그인하면 대시보드로 이동한다', async ({ page }) => {
    // Arrange & Act
    await page.getByLabel('이메일').fill('[email protected]');
    await page.getByLabel('비밀번호').fill('SecurePass123!');
    await page.getByRole('button', { name: '로그인' }).click();

    // Assert
    await expect(page).toHaveURL('/dashboard');
    await expect(page.getByText('환영합니다')).toBeVisible();
  });

  test('잘못된 비밀번호로 로그인하면 에러 메시지를 표시한다', async ({ page }) => {
    await page.getByLabel('이메일').fill('[email protected]');
    await page.getByLabel('비밀번호').fill('wrong-password');
    await page.getByRole('button', { name: '로그인' }).click();

    await expect(page.getByText('이메일 또는 비밀번호가 올바르지 않습니다')).toBeVisible();
    await expect(page).toHaveURL('/login');
  });

  test('빈 필드로 로그인하면 유효성 검사 메시지를 표시한다', async ({ page }) => {
    await page.getByRole('button', { name: '로그인' }).click();

    await expect(page.getByText('이메일을 입력해주세요')).toBeVisible();
    await expect(page.getByText('비밀번호를 입력해주세요')).toBeVisible();
  });

  test('이메일만 입력하면 비밀번호 필드 유효성 검사를 표시한다', async ({ page }) => {
    await page.getByLabel('이메일').fill('[email protected]');
    await page.getByRole('button', { name: '로그인' }).click();

    await expect(page.getByText('비밀번호를 입력해주세요')).toBeVisible();
  });

  test('Google 소셜 로그인 버튼을 클릭하면 OAuth 페이지로 이동한다', async ({ page }) => {
    const [popup] = await Promise.all([
      page.waitForEvent('popup'),
      page.getByRole('button', { name: 'Google로 로그인' }).click(),
    ]);

    await expect(popup).toHaveURL(/accounts\.google\.com/);
  });

  test('비밀번호 찾기 링크를 클릭하면 비밀번호 재설정 페이지로 이동한다', async ({ page }) => {
    await page.getByRole('link', { name: '비밀번호 찾기' }).click();
    await expect(page).toHaveURL('/reset-password');
  });

  test('로그인 상태에서 로그인 페이지에 접근하면 대시보드로 리다이렉트한다', async ({ page, context }) => {
    // 로그인 쿠키 설정
    await context.addCookies([{
      name: 'session',
      value: 'valid-session-token',
      domain: 'localhost',
      path: '/',
    }]);

    await page.goto('/login');
    await expect(page).toHaveURL('/dashboard');
  });

  test('비밀번호 입력 필드에 비밀번호 표시/숨김 토글이 동작한다', async ({ page }) => {
    const passwordInput = page.getByLabel('비밀번호');
    await passwordInput.fill('test-password');

    // 기본: 비밀번호 숨김
    await expect(passwordInput).toHaveAttribute('type', 'password');

    // 표시 버튼 클릭
    await page.getByRole('button', { name: '비밀번호 보기' }).click();
    await expect(passwordInput).toHaveAttribute('type', 'text');
  });
});

E2E 테스트 설정 팁

Playwright E2E 테스트의 효과를 극대화하기 위해 다음 설정을 권장합니다.

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

export default defineConfig({
  testDir: './e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: [
    ['html', { open: 'never' }],
    ['json', { outputFile: 'test-results/results.json' }],
  ],
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } },
    { name: 'mobile-chrome', use: { ...devices['Pixel 5'] } },
    { name: 'mobile-safari', use: { ...devices['iPhone 13'] } },
  ],
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
});

테스트 접근법 상세 비교

각 테스트 방법의 장단점을 더 자세히 비교해보겠습니다. 프로젝트의 특성에 따라 적절한 조합을 선택하는 것이 중요합니다.

특성 /test 명령어 TDD 에이전트 커스텀 스킬 MCP 통합 Playwright E2E
설정 시간 0분 10~15분 20~30분 30~60분 15~30분
실행 속도 매우 빠름 보통 빠름 느림 느림
유지보수 비용 낮음 보통 보통 높음 높음
버그 탐지 범위 함수 내부 로직 모듈 간 연동 팀 규칙 준수 시스템 간 연동 사용자 시나리오
CI/CD 적합도 매우 높음 높음 높음 보통 보통
프레임워크 Jest/Pytest Jest/Pytest 팀 선택 Pytest/Jest Playwright

커버리지 향상 전략

Claude Code를 활용한 테스트 자동 생성은 시작에 불과합니다. 장기적으로 높은 테스트 커버리지를 유지하려면 체계적인 전략이 필요합니다.

1단계: 현재 커버리지 측정

먼저 현재 프로젝트의 테스트 커버리지를 정확히 측정합니다.

# JavaScript/TypeScript (Jest)
npx jest --coverage --coverageReporters=text-summary

# Python (Pytest + coverage)
pytest --cov=src --cov-report=term-missing

2단계: 우선순위 파일 식별

Claude Code에게 커버리지가 낮은 파일 중 비즈니스 영향도가 높은 파일을 식별하도록 요청합니다.

# Claude Code에서 실행
> 커버리지 리포트를 분석하고, 테스트가 필요한 파일을 우선순위별로 정리해줘.
> 기준: 1) 비즈니스 로직 중요도 2) 변경 빈도 3) 현재 커버리지

3단계: 점진적 커버리지 확대

한 번에 모든 파일에 테스트를 추가하는 것보다 점진적으로 확대하는 전략이 효과적입니다.

4단계: CI/CD 파이프라인에 통합

테스트를 CI/CD 파이프라인에 통합하여 자동으로 실행되도록 합니다.

# .github/workflows/test.yml
name: Test Suite
on: [push, pull_request]

jobs:
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npx jest --coverage --ci
      - name: Check coverage threshold
        run: |
          COVERAGE=$(npx jest --coverage --coverageReporters=json-summary \
            | jq '.total.lines.pct')
          if (( $(echo "$COVERAGE < 80" | bc -l) )); then
            echo "Coverage $COVERAGE% is below 80% threshold"
            exit 1
          fi

  e2e-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npx playwright test
      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: playwright-report
          path: playwright-report/

실전 팁: 더 나은 테스트를 생성하는 방법

Claude Code로 테스트를 생성할 때 더 높은 품질의 테스트를 얻기 위한 실전 팁들입니다.

구체적인 프롬프트 작성

막연한 요청보다 구체적인 요구사항을 제시하면 더 좋은 테스트를 얻을 수 있습니다.

# 나쁜 예
> 이 파일 테스트해줘

# 좋은 예
> src/services/payment.ts의 processPayment 함수에 대해:
> - 정상 결제 성공 케이스
> - 카드 잔액 부족 에러
> - 네트워크 타임아웃 재시도 로직
> - 동시 결제 요청 처리
> 테스트를 Jest로 생성해줘. Mock은 stripe 라이브러리만 적용.

기존 테스트 패턴 참조

프로젝트에 이미 테스트가 있다면, 기존 패턴을 참조하도록 지시합니다.

> src/__tests__/auth.test.ts의 패턴을 따라서
> src/services/notification.ts에 대한 테스트를 생성해줘.

테스트 리뷰 자동화

생성된 테스트의 품질을 자동으로 검증하는 습관을 들이세요.

# 생성된 테스트 실행
npx jest src/__tests__/payment.test.ts --verbose

# 뮤테이션 테스트로 테스트 품질 검증
npx stryker run --mutate src/services/payment.ts

좋은 테스트는 구현이 아닌 행동을 검증합니다. Claude Code가 생성한 테스트도 내부 구현에 과도하게 의존하지 않는지 반드시 검토하세요. 리팩토링 후에도 테스트가 유효해야 좋은 테스트입니다.

자주 묻는 질문(FAQ)

Claude Code가 생성한 테스트를 그대로 사용해도 되나요?

대부분의 경우 바로 사용할 수 있습니다. 다만 프로젝트의 특수한 설정(환경변수, 외부 의존성 등)에 따라 약간의 수정이 필요할 수 있습니다. 생성된 테스트를 실행해보고 실패하는 부분이 있으면 수정하세요.

어떤 테스트 프레임워크를 지원하나요?

Claude Code는 주요 테스트 프레임워크를 모두 지원합니다. JavaScript/TypeScript의 Jest, Vitest, Mocha, Python의 Pytest, unittest, Go의 testing 패키지, Java의 JUnit, Kotlin의 Kotest 등 대부분의 언어와 프레임워크에서 테스트를 생성할 수 있습니다.

테스트 커버리지 100%를 목표로 해야 하나요?

현실적으로 100% 커버리지를 목표로 하는 것은 비효율적입니다. 핵심 비즈니스 로직은 90% 이상, 유틸리티 함수는 80% 이상, 전체 프로젝트는 70% 이상을 권장합니다. 중요한 것은 커버리지 숫자가 아니라 의미 있는 검증을 하는 테스트를 작성하는 것입니다.

관련 리소스