AI 기반 테스트

AI로 테스트 코드를 생성하고 테스트 자동화하는 방법을 배웁니다. 단위 테스트, 통합 테스트, E2E 테스트를 AI로 만들어보세요.

개요

AI를 활용하면 테스트 코드를 더 빠르고 정확하게 작성할 수 있습니다. 이 가이드에서는 AI로 다양한 종류의 테스트를 생성하고 자동화하는 방법을 배웁니다.

📋 테스트 종류
  • 단위 테스트 (Unit Test): 개별 함수/메서드 테스트
  • 통합 테스트 (Integration Test): 여러 모듈 간의 상호작용 테스트
  • E2E 테스트 (End-to-End): 사용자 관점의 전체 플로우 테스트

단위 테스트 생성

개별 함수나 메서드에 대한 단위 테스트를 AI로 생성할 수 있습니다.

테스트 생성 요청

AI에게 요청
다음 JavaScript 함수의 단위 테스트를 만들어줘:

```javascript
function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

function multiply(a, b) {
  return a * b;
}
```

Jest를 사용하고, 정상적인 값과
에지 케이스를 모두 테스트해줘.

생성된 테스트 코드

JavaScript (calculator.test.js)
const { add, subtract, multiply } = require('./calculator');

describe('Calculator', () => {
  
  describe('add', () => {
    test('두 양수의 합', () => {
      expect(add(2, 3)).toBe(5);
    });

    test('음수 더하기', () => {
      expect(add(-5, 3)).toBe(-2);
    });

    test('0 더하기', () => {
      expect(add(0, 10)).toBe(10);
    });
  });

  describe('subtract', () => {
    test('두 양수의 차', () => {
      expect(subtract(10, 3)).toBe(7);
    });
  });

  describe('multiply', () => {
    test('두 양수의 곱', () => {
      expect(multiply(4, 5)).toBe(20);
    });

    test('0과 곱하기', () => {
      expect(multiply(100, 0)).toBe(0);
    });
  });
});

통합 테스트 생성

여러 모듈이 함께 동작하는 것을 테스트합니다.

테스트 생성 요청

AI에게 요청
다음 Express.js API의 통합 테스트를 만들어줘:

- GET /api/users - 모든 사용자 조회
- GET /api/users/:id - 특정 사용자 조회
- POST /api/users - 사용자 생성
- DELETE /api/users/:id - 사용자 삭제

supertest를 사용하고, 각 엔드포인트를
테스트해줘.
💡 팁

통합 테스트는 실제 데이터베이스나 외부 API를 모의 객체(mock)로 대체하여 테스트합니다.

E2E 테스트 생성

사용자 관점에서 전체 애플리케이션 플로우를 테스트합니다.

Playwright 설치

Bash
# 설치
npm init playwright@latest

# 또는 기존 프로젝트에 추가
npm install -D @playwright/test
npx playwright install  # 브라우저 다운로드

Playwright 실제 E2E 테스트 코드

TypeScript (e2e/todo-flow.spec.ts)
import { test, expect, type Page } from '@playwright/test';

// 페이지 객체 헬퍼
async function login(page: Page, email: string, password: string) {
  await page.goto('/login');
  await page.getByLabel('이메일').fill(email);
  await page.getByLabel('비밀번호').fill(password);
  await page.getByRole('button', { name: '로그인' }).click();
  await expect(page).toHaveURL('/dashboard');
}

test.describe('투두 관리 E2E 플로우', () => {
  test.beforeEach(async ({ page }) => {
    await login(page, 'test@example.com', 'password123');
  });

  test('투두 추가 → 완료 표시 → 삭제 전체 플로우', async ({ page }) => {
    // 1. 투두 추가
    const todoText = `테스트 투두 ${Date.now()}`;
    await page.getByPlaceholder('새 항목 추가...').fill(todoText);
    await page.keyboard.press('Enter');

    // 2. 투두가 목록에 나타나는지 확인
    const todoItem = page.getByText(todoText);
    await expect(todoItem).toBeVisible();

    // 3. 완료 체크박스 클릭
    await todoItem.locator('xpath=../..').getByRole('checkbox').click();
    await expect(todoItem).toHaveClass(/completed/);

    // 4. 삭제 버튼 클릭
    await todoItem.locator('xpath=../..').getByRole('button', { name: '삭제' }).click();
    await expect(todoItem).not.toBeVisible();
  });

  test('빈 입력으로 투두 추가 시 에러 표시', async ({ page }) => {
    await page.getByRole('button', { name: '추가' }).click();
    await expect(page.getByText('내용을 입력해주세요')).toBeVisible();
  });

  test('로그아웃 후 리다이렉트', async ({ page }) => {
    await page.getByRole('button', { name: '로그아웃' }).click();
    await expect(page).toHaveURL('/login');
  });
});

// API 모킹 예시
test('API 오류 시 에러 메시지 표시', async ({ page }) => {
  // 서버 응답을 모킹
  await page.route('**/api/todos', route => {
    route.fulfill({ status: 500, body: 'Internal Server Error' });
  });

  await page.goto('/dashboard');
  await expect(page.getByText('데이터를 불러올 수 없습니다')).toBeVisible();
});

Playwright 설정 (playwright.config.ts)

TypeScript
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './e2e',
  fullyParallel: true,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'Mobile Chrome', use: { ...devices['Pixel 5'] } },
  ],
  webServer: {
    command: 'npm run start',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
});

테스트 커버리지 측정

코드 커버리지를 측정하여 테스트 누락 영역을 파악합니다.

Jest 커버리지 설정

JSON (package.json)
{
  "scripts": {
    "test": "jest",
    "test:coverage": "jest --coverage",
    "test:watch": "jest --watch"
  },
  "jest": {
    "coverageThreshold": {
      "global": {
        "branches": 70,
        "functions": 80,
        "lines": 80,
        "statements": 80
      }
    },
    "coverageReporters": ["text", "lcov", "html"],
    "collectCoverageFrom": [
      "src/**/*.{js,ts}",
      "!src/**/*.d.ts",
      "!src/main.ts"
    ]
  }
}
Bash — 커버리지 리포트 생성
# 커버리지 측정 + 리포트 생성
npm run test:coverage

# 출력 예시:
# -------|---------|----------|---------|---------|
# File   | % Stmts | % Branch | % Funcs | % Lines |
# -------|---------|----------|---------|---------|
# All    |   82.5  |   75.0   |   85.0  |   82.5  |

# HTML 리포트 열기
open coverage/lcov-report/index.html

테스트 자동화

CI/CD 파이프라인에 테스트를 통합하여 자동화합니다.

GitHub Actions에서 테스트 자동화

YAML
name: Test

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Run unit tests
        run: npm run test:unit
        
      - name: Run integration tests
        run: npm run test:integration
        
      - name: Run E2E tests
        run: npm run test:e2e
⚠️ 주의

E2E 테스트는 데이터베이스와 외부 서비스를 필요로 할 수 있으므로, CI 환경에서 적절히 설정해야 합니다.

테스트 생성 프롬프트 팁

더 나은 테스트를 생성하기 위한 프롬프트 작성 팁입니다.

효과적인 프롬프트 작성법
  • 코드 공유: 테스트할 함수의 전체 코드 제공
  • 테스트 프레임워크 명시: Jest, Mocha, Pytest 등
  • 에지 케이스 언급: null, undefined, 빈 값 등
  • 기대 결과 명시: 어떤 결과가 나와야 하는지
  • 기존 테스트 참조: 프로젝트의 테스트 스타일을 예시로 제공

좋은 프롬프트 예시

좋은 프롬프트 예시
다음 validateEmail 함수의 Jest 단위 테스트를 만들어줘.
함수 코드:
function validateEmail(email) {
  const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return regex.test(email);
}

요구사항:
1. 유효한 이메일 주소 테스트
2. 유효하지 않은 이메일 주소 테스트 (@ 없음, 도메인 없음)
3. 빈 문자열 테스트
4. null, undefined 입력 테스트

나쁜 프롬프트 예시와 개선

나쁜 예시
// 모호한 요청 → 결과 품질 낮음
"이 코드 테스트 만들어줘"

// 프레임워크 미지정 → 불일치한 결과
"이 함수 테스트해줘"
개선된 예시
// 구체적인 요청 → 고품질 결과
"src/utils/dateFormatter.ts의 formatDate 함수에 대해
Jest + TypeScript 단위 테스트를 작성해줘.

테스트 항목:
1. ISO 8601 형식 입력 → 'YYYY년 MM월 DD일' 출력
2. Unix timestamp 입력 → 올바른 날짜 문자열
3. 유효하지 않은 날짜 → Error throw
4. 타임존 처리 (UTC, KST)

프로젝트 테스트 스타일 참고:
```typescript
describe('모듈명', () => {
  it('should 동작 설명 when 조건', () => {
    // Arrange → Act → Assert 패턴
  });
});
```"

AI를 활용한 Mock/Stub 생성

외부 API, 데이터베이스, 파일 시스템 등 외부 의존성을 Mock으로 대체하는 것은 테스트 작성에서 가장 어려운 부분 중 하나입니다. AI를 활용하면 복잡한 Mock 설정을 빠르게 생성할 수 있습니다.

API Mock 생성 예시

TypeScript (API Mock 테스트)
import { jest } from '@jest/globals';
import { fetchUserProfile } from '../services/userService';
import * as api from '../utils/httpClient';

// httpClient 모듈 전체를 Mock으로 대체
jest.mock('../utils/httpClient');
const mockedApi = api as jest.Mocked<typeof api>;

describe('UserService', () => {
  afterEach(() => {
    jest.resetAllMocks();
  });

  test('정상적인 프로필 조회', async () => {
    // Arrange: Mock 응답 설정
    const mockUser = {
      id: 1,
      name: '홍길동',
      email: 'hong@example.com',
      role: 'admin'
    };
    mockedApi.get.mockResolvedValue({ data: mockUser });

    // Act
    const result = await fetchUserProfile(1);

    // Assert
    expect(result.name).toBe('홍길동');
    expect(mockedApi.get).toHaveBeenCalledWith('/api/users/1');
    expect(mockedApi.get).toHaveBeenCalledTimes(1);
  });

  test('네트워크 오류 시 재시도', async () => {
    // 첫 번째 호출 실패, 두 번째 성공
    mockedApi.get
      .mockRejectedValueOnce(new Error('Network Error'))
      .mockResolvedValueOnce({ data: { id: 1, name: '홍길동' } });

    const result = await fetchUserProfile(1);

    expect(result.name).toBe('홍길동');
    expect(mockedApi.get).toHaveBeenCalledTimes(2);
  });

  test('404 응답 시 null 반환', async () => {
    mockedApi.get.mockRejectedValue({
      response: { status: 404 }
    });

    const result = await fetchUserProfile(999);

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

데이터베이스 Mock 예시

Python (pytest + Mock)
import pytest
from unittest.mock import MagicMock, patch
from services.order_service import create_order

class TestOrderService:
    """주문 서비스 테스트 (DB Mock 활용)"""

    @patch("services.order_service.db")
    def test_create_order_success(self, mock_db):
        """정상 주문 생성 테스트"""
        # Arrange: DB Mock 설정
        mock_db.products.find_one.return_value = {
            "id": 1, "name": "키보드",
            "price": 50000, "stock": 10
        }
        mock_db.orders.insert_one.return_value = MagicMock(
            inserted_id="order-001"
        )

        # Act
        result = create_order(product_id=1, quantity=2)

        # Assert
        assert result["order_id"] == "order-001"
        assert result["total"] == 100000
        mock_db.products.update_one.assert_called_once()

    @patch("services.order_service.db")
    def test_create_order_out_of_stock(self, mock_db):
        """재고 부족 시 에러 테스트"""
        mock_db.products.find_one.return_value = {
            "id": 1, "name": "키보드",
            "price": 50000, "stock": 0
        }

        with pytest.raises(ValueError, match="재고 부족"):
            create_order(product_id=1, quantity=2)

스냅샷 테스트와 AI

스냅샷 테스트는 컴포넌트의 출력이 의도치 않게 변경되는 것을 감지합니다. AI를 활용하면 초기 스냅샷 생성과 스냅샷 변경 분석을 자동화할 수 있습니다.

TypeScript (React 스냅샷 테스트)
import { render } from '@testing-library/react';
import { UserCard } from '../components/UserCard';

describe('UserCard 스냅샷 테스트', () => {
  test('기본 렌더링 스냅샷', () => {
    const { container } = render(
      <UserCard
        name="홍길동"
        email="hong@example.com"
        role="admin"
      />
    );
    expect(container).toMatchSnapshot();
  });

  test('프로필 이미지 없을 때 스냅샷', () => {
    const { container } = render(
      <UserCard
        name="김철수"
        email="kim@example.com"
        role="user"
        avatarUrl={undefined}
      />
    );
    expect(container).toMatchSnapshot();
  });

  test('긴 이름 처리 스냅샷', () => {
    const { container } = render(
      <UserCard
        name="매우 긴 이름을 가진 사용자의 프로필 카드"
        email="longname@example.com"
        role="user"
      />
    );
    expect(container).toMatchSnapshot();
  });
});

AI 테스트 생성 워크플로우

AI를 활용한 테스트 생성의 전체 워크플로우를 정리합니다. 코드 분석부터 테스트 작성, 검증, 피드백까지의 반복 과정입니다.

AI 테스트 생성 워크플로우 1. 코드 분석 함수/모듈 파악 2. 프롬프트 작성 코드 + 요구사항 3. AI 테스트 생성 코드 자동 생성 4. 실행 및 검증 테스트 실행 통과? Yes 완료 No: 실패 로그 포함하여 재요청 AI 테스트 생성 시 핵심 포인트 코드 전체를 제공 프레임워크 명시 에지 케이스 요청

AI 테스트 생성 워크플로우: 코드 분석 → 프롬프트 → 생성 → 검증 → 피드백 반복

AI 생성 테스트의 주의사항
  • 맹목적 신뢰 금지: AI가 생성한 테스트가 항상 올바른 것은 아닙니다. 반드시 실행하고 결과를 확인하세요.
  • 비즈니스 로직 검증: AI는 함수의 동작은 테스트하지만, 비즈니스 요구사항의 정확성까지 보장하지 않습니다.
  • 테스트 독립성: 생성된 테스트 간 의존성이 없는지 확인하세요. 각 테스트는 독립적으로 실행 가능해야 합니다.
  • 하드코딩된 값: AI가 예시로 넣은 상수값이 실제 환경과 맞는지 검토하세요.

테스트 피라미드

효과적인 테스트 전략을 위한 테스트 피라미드 구조입니다:

E2E 테스트 (소수) 통합 테스트 (중간) 단위 테스트 (대부분) 빠름 + 저렴 느림 + 비쌈

테스트 피라미드: 단위 테스트를 가장 많게, E2E를 가장 적게

테스트 유형 비율 실행 속도 AI 활용 효과 적합한 프롬프트 전략
단위 테스트 70% 매우 빠름 (ms) 높음 (반복 패턴 자동화) 함수 코드 + 에지 케이스 목록 제공
통합 테스트 20% 보통 (초) 중간 (Mock 설정 자동화) 모듈 간 인터페이스 + 시나리오 제공
E2E 테스트 10% 느림 (분) 높음 (UI 상호작용 자동화) 사용자 시나리오 + 페이지 구조 제공

속성 기반 테스트 (Property-Based Testing)

속성 기반 테스트는 구체적인 입출력 대신 "항상 참이어야 하는 속성"을 정의하고, 랜덤 입력으로 속성 위반을 탐지합니다. AI에게 속성을 설명하면 효과적인 속성 기반 테스트를 생성할 수 있습니다.

Python (Hypothesis를 활용한 속성 기반 테스트)
from hypothesis import given, strategies as st
from hypothesis import assume
from utils.math_utils import safe_divide, clamp

@given(a=st.integers(), b=st.integers())
def test_safe_divide_never_raises(a, b):
    """safe_divide는 어떤 입력에도 예외를 발생시키지 않아야 함"""
    result = safe_divide(a, b)
    assert result is not None  # 항상 값을 반환

@given(a=st.integers(), b=st.integers(min_value=1))
def test_safe_divide_correct(a, b):
    """0이 아닌 수로 나눌 때 결과가 올바라야 함"""
    result = safe_divide(a, b)
    assert abs(result * b - a) < 1e-9

@given(
    value=st.floats(allow_nan=False, allow_infinity=False),
    min_val=st.floats(allow_nan=False, allow_infinity=False),
    max_val=st.floats(allow_nan=False, allow_infinity=False)
)
def test_clamp_always_in_range(value, min_val, max_val):
    """clamp 결과는 항상 [min, max] 범위 내에 있어야 함"""
    assume(min_val <= max_val)
    result = clamp(value, min_val, max_val)
    assert min_val <= result <= max_val

다음 단계

테스트 자동화에 대해 더 자세히 배워보세요!

프롬프트 테스트

프롬프트의 품질을 테스트하는 방법을 배워보세요

프롬프트 테스트 →

CI/CD 파이프라인

테스트를 CI/CD에 통합하세요

CI/CD 파이프라인 →

보안 모범 사례

보안 테스트도자동화하세요

보안 모범 사례 →

핵심 정리

  • 단위 테스트: 개별 함수/메서드를 테스트, 가장 빠르고 저렴
  • 통합 테스트: 여러 모듈 간 상호작용 테스트
  • E2E 테스트: 사용자 관점의 전체 플로우 테스트
  • 테스트 피라미드: 단위 테스트를 가장 많이 작성
  • CI/CD 통합: 푸시 시 자동으로 테스트 실행