Choorai
Unit pytest Vitest

단위 테스트 가이드

개별 함수, API 엔드포인트, 컴포넌트를 격리하여 테스트합니다. 빠르게 실행되고 디버깅이 쉬운 것이 장점입니다.

단위 테스트의 원칙

  • Fast: 밀리초 단위로 실행
  • Isolated: 다른 테스트에 영향 없음
  • Repeatable: 언제 실행해도 같은 결과
  • Self-validating: 성공/실패가 명확

Python: pytest

설치 및 설정

터미널
# 의존성 설치
pip install pytest pytest-asyncio httpx

# pytest.ini (설정 파일)
[pytest]
testpaths = tests
asyncio_mode = auto

API 테스트 예제

tests/test_projects.py
# tests/test_projects.py
import pytest
from fastapi.testclient import TestClient
from app.main import app

client = TestClient(app)

def test_create_project():
    """프로젝트 생성 테스트"""
    response = client.post(
        "/api/v1/projects",
        json={"name": "Test Project", "description": "A test"}
    )
    assert response.status_code == 201
    data = response.json()
    assert data["name"] == "Test Project"
    assert "id" in data

def test_create_project_validation():
    """이름 필드 검증 테스트"""
    response = client.post("/api/v1/projects", json={})
    assert response.status_code == 422  # Validation error

def test_get_projects_empty():
    """빈 목록 반환 테스트"""
    response = client.get("/api/v1/projects")
    assert response.status_code == 200
    assert response.json()["items"] == []

테스트 실행

터미널
# 전체 테스트 실행
pytest

# 상세 출력
pytest -v

# 특정 파일만
pytest tests/test_projects.py

# 특정 함수만
pytest tests/test_projects.py::test_create_project

# 커버리지 확인
pip install pytest-cov
pytest --cov=app --cov-report=html

FastAPI TestClient

FastAPI의 TestClient는 실제 HTTP 요청 없이 API를 테스트할 수 있습니다. 비동기 테스트가 필요하면 httpx.AsyncClient를 사용하세요.

Node.js: Vitest

설치 및 설정

터미널
# 의존성 설치
npm install -D vitest

# package.json scripts
{
  "scripts": {
    "test": "vitest",
    "test:run": "vitest run",
    "test:coverage": "vitest run --coverage"
  }
}
vitest.config.ts
// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: false,
    environment: 'node', // 또는 'jsdom' (브라우저 환경)
  },
});

Hono API 테스트 예제

src/index.test.ts
// src/index.test.ts
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { app } from './index';

describe('Todos API', () => {
  // 각 테스트 전에 저장소 초기화
  beforeEach(() => {
    // Reset storage
  });

  describe('POST /api/v1/todos', () => {
    it('should create a todo', async () => {
      const res = await app.request('/api/v1/todos', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ title: 'Test Todo' }),
      });

      expect(res.status).toBe(201);
      const data = await res.json();
      expect(data.title).toBe('Test Todo');
      expect(data.is_completed).toBe(false);
    });

    it('should return 422 for empty title', async () => {
      const res = await app.request('/api/v1/todos', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ title: '' }),
      });

      expect(res.status).toBe(422); // Validation error
    });
  });

  describe('PATCH /api/v1/todos/:id', () => {
    it('should toggle todo completion', async () => {
      // Create a todo first
      const createRes = await app.request('/api/v1/todos', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ title: 'Test' }),
      });
      const { id } = await createRes.json();

      // Toggle completion
      const res = await app.request(`/api/v1/todos/${id}`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ is_completed: true }),
      });

      expect(res.status).toBe(200);
      const data = await res.json();
      expect(data.is_completed).toBe(true);
    });
  });
});

Hono의 app.request()

Hono는 app.request() 메서드로 실제 서버 없이 HTTP 요청을 시뮬레이션할 수 있습니다.

Vue: Vitest + @vue/test-utils

설정

설정
# 의존성 설치
npm install -D vitest @vue/test-utils jsdom

# vitest.config.ts
import { defineConfig } from 'vitest/config';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [vue()],
  test: {
    environment: 'jsdom',  // 브라우저 환경 시뮬레이션
    globals: false,
  },
});

Composable 테스트 예제

src/__tests__/useTodos.test.ts
// src/__tests__/useTodos.test.ts
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { flushPromises } from '@vue/test-utils';
import { QueryClient, VueQueryPlugin } from '@tanstack/vue-query';
import { createApp, h, defineComponent } from 'vue';
import { useTodos, useCreateTodo } from '../composables/useTodos';

// Vue composable 테스트를 위한 헬퍼
function withSetup<T>(composable: () => T) {
  let result!: T;

  const TestComponent = defineComponent({
    setup() {
      result = composable();
      return () => h('div');
    },
  });

  const queryClient = new QueryClient({
    defaultOptions: {
      queries: { retry: false },
      mutations: { retry: false },
    },
  });

  const app = createApp(TestComponent);
  app.use(VueQueryPlugin, { queryClient });
  app.mount(document.createElement('div'));

  return { result, app };
}

describe('useTodos', () => {
  beforeEach(() => {
    vi.restoreAllMocks();
  });

  it('should fetch todos list', async () => {
    const mockResponse = {
      items: [{ id: '1', title: 'Todo 1', is_completed: false }],
      total: 1,
    };

    global.fetch = vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve(mockResponse),
    });

    const { result, app } = withSetup(() => useTodos());

    await flushPromises();
    await new Promise(resolve => setTimeout(resolve, 100));

    expect(result.data.value).toEqual(mockResponse);
    app.unmount();
  });
});

jsdom 환경 필수

Vue 컴포넌트와 composable 테스트에는 environment: 'jsdom'이 필요합니다. 설정하지 않으면 document is not defined 에러가 발생합니다.

React: Vitest + @testing-library/react

설정

터미널
# 의존성 설치
npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom

Hook 테스트 예제

src/__tests__/useProjects.test.ts
// src/__tests__/useProjects.test.ts
import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useProjects, useCreateProject } from '../hooks/useProjects';

const createWrapper = () => {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: { retry: false },
    },
  });
  return ({ children }) => (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  );
};

describe('useProjects', () => {
  it('should fetch projects', async () => {
    const mockData = { items: [{ id: '1', name: 'Test' }], total: 1 };

    global.fetch = vi.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve(mockData),
    });

    const { result } = renderHook(() => useProjects(), {
      wrapper: createWrapper(),
    });

    await waitFor(() => {
      expect(result.current.data).toEqual(mockData);
    });
  });
});

모킹 (Mocking)

외부 의존성(API, 데이터베이스)을 모킹하여 격리된 테스트를 작성합니다.

Vitest에서 fetch 모킹

모킹 예제
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';

// 전역 fetch 모킹
const originalFetch = global.fetch;

beforeEach(() => {
  global.fetch = vi.fn();
});

afterEach(() => {
  global.fetch = originalFetch;
});

it('should handle API response', async () => {
  // 성공 응답 모킹
  vi.mocked(global.fetch).mockResolvedValue({
    ok: true,
    json: () => Promise.resolve({ data: 'test' }),
  } as Response);

  // 테스트 실행
  const result = await fetchData();
  expect(result.data).toBe('test');
});

it('should handle API error', async () => {
  // 에러 응답 모킹
  vi.mocked(global.fetch).mockResolvedValue({
    ok: false,
    status: 500,
  } as Response);

  // 에러 처리 검증
  await expect(fetchData()).rejects.toThrow();
});

베스트 프랙티스

DO

  • ✅ 하나의 테스트에 하나의 검증
  • ✅ 테스트 이름에 의도 명시
  • ✅ 외부 의존성 모킹
  • ✅ 엣지 케이스 테스트
  • ✅ 에러 상황 테스트

DON'T

  • ❌ 테스트 간 상태 공유
  • ❌ 실제 API/DB 호출
  • ❌ sleep/setTimeout 의존
  • ❌ 구현 세부사항 테스트
  • ❌ 매직 넘버 사용

다음 단계

단위 테스트를 마스터했다면, E2E 테스트 가이드에서 Playwright로 전체 사용자 시나리오를 테스트하는 방법을 배웁니다.

마지막 업데이트: 2026년 2월 22일 · 버전: v0.0.1

피드백 보내기

입력한 내용으로 새 이슈 페이지를 엽니다.

GitHub 이슈로 보내기