Skip to main content

테스트 기초

학습 목표

  • pytest로 테스트를 작성하고 실행할 수 있다
  • fixture로 테스트 데이터와 환경을 관리할 수 있다
  • parametrize로 여러 입력에 대한 테스트를 효율적으로 작성할 수 있다
  • ML 코드에 적합한 테스트 전략을 적용할 수 있다

왜 중요한가

ML/DL 코드에서 “학습이 안 되는” 원인은 데이터 전처리 버그, 손실 함수 오류, 차원 불일치 등 다양합니다. 자동화된 테스트는 코드 변경 시 기존 기능이 깨지지 않았음을 보장하고, 리팩토링을 안전하게 해줍니다. pytest는 Python에서 가장 널리 사용되는 테스트 프레임워크입니다.
# pytest 설치
pip install pytest

pytest 기본

1

첫 번째 테스트 작성

# math_utils.py
def add(a: float, b: float) -> float:
    return a + b

def divide(a: float, b: float) -> float:
    if b == 0:
        raise ValueError("0으로 나눌 수 없습니다")
    return a / b
# test_math_utils.py
from math_utils import add, divide
import pytest

def test_add():
    assert add(2, 3) == 5
    assert add(-1, 1) == 0
    assert add(0.1, 0.2) == pytest.approx(0.3)  # 부동소수점 비교

def test_divide():
    assert divide(10, 2) == 5.0
    assert divide(7, 3) == pytest.approx(2.333, rel=1e-3)

def test_divide_by_zero():
    with pytest.raises(ValueError, match="0으로 나눌 수 없습니다"):
        divide(10, 0)
# 테스트 실행
pytest test_math_utils.py -v
2

테스트 파일 구조

project/
├── src/
│   ├── math_utils.py
│   ├── data_loader.py
│   └── model.py
└── tests/
    ├── test_math_utils.py
    ├── test_data_loader.py
    └── test_model.py
pytest는 test_로 시작하는 파일과 함수를 자동으로 탐색합니다. 테스트 파일은 tests/ 디렉토리에 모아두는 것이 관례입니다.
3

assert 패턴

import pytest

# 값 비교
assert result == expected
assert result != unexpected

# 부동소수점 비교 (오차 허용)
assert 0.1 + 0.2 == pytest.approx(0.3)
assert loss == pytest.approx(0.05, abs=1e-3)   # 절대 오차
assert accuracy == pytest.approx(0.95, rel=0.01)  # 상대 오차 1%

# 컬렉션
assert len(results) == 10
assert "key" in config
assert item not in blacklist

# 타입 확인
assert isinstance(model, dict)

# 예외 확인
with pytest.raises(TypeError):
    add("a", 1)

# 경고 확인
with pytest.warns(UserWarning):
    deprecated_function()

Fixture - 테스트 환경 관리

import pytest
from pathlib import Path
import json

# 기본 fixture
@pytest.fixture
def sample_config():
    """테스트용 설정 딕셔너리를 제공합니다."""
    return {
        "model": "bert-base",
        "learning_rate": 0.001,
        "epochs": 10,
        "batch_size": 32,
    }

def test_config_has_model(sample_config):
    assert "model" in sample_config
    assert sample_config["learning_rate"] > 0

# 리소스 관리 fixture (setup + teardown)
@pytest.fixture
def temp_config_file(tmp_path):
    """임시 설정 파일을 생성하고 테스트 후 자동 정리합니다."""
    config = {"model": "gpt2", "epochs": 5}
    filepath = tmp_path / "config.json"
    filepath.write_text(json.dumps(config), encoding="utf-8")
    yield filepath  # yield 이전 = setup, 이후 = teardown
    # tmp_path는 pytest가 자동 정리

def test_load_config(temp_config_file):
    with open(temp_config_file, "r") as f:
        config = json.load(f)
    assert config["model"] == "gpt2"

# 스코프 지정 fixture
@pytest.fixture(scope="session")
def heavy_resource():
    """세션 전체에서 한 번만 생성되는 무거운 리소스"""
    print("리소스 생성 (비용이 큰 작업)")
    resource = {"data": list(range(10000))}
    yield resource
    print("리소스 정리")

Fixture 스코프

스코프설명사용 예시
function매 테스트마다 생성 (기본값)독립적인 테스트 데이터
class클래스당 한 번클래스 내 공유 데이터
module모듈당 한 번파일 수준 공유 리소스
session전체 세션에서 한 번데이터베이스 연결, 모델 로딩

Parametrize - 다중 입력 테스트

import pytest

# 여러 입력값에 대해 같은 테스트 실행
@pytest.mark.parametrize("a, b, expected", [
    (2, 3, 5),
    (0, 0, 0),
    (-1, 1, 0),
    (100, 200, 300),
    (0.1, 0.2, 0.3),
])
def test_add_parametrize(a, b, expected):
    result = add(a, b)
    assert result == pytest.approx(expected)

# 예외 케이스 포함
@pytest.mark.parametrize("a, b, expected_error", [
    (10, 0, ValueError),
    ("a", 1, TypeError),
])
def test_divide_errors(a, b, expected_error):
    with pytest.raises(expected_error):
        divide(a, b)

# 활성화 함수 테스트
@pytest.mark.parametrize("input_val, expected", [
    (0.0, 0.5),
    (100.0, 1.0),   # 매우 큰 값
    (-100.0, 0.0),  # 매우 작은 값
])
def test_sigmoid(input_val, expected):
    import math
    result = 1 / (1 + math.exp(-input_val))
    assert result == pytest.approx(expected, abs=1e-4)

마킹과 필터링

import pytest

# 테스트 마킹
@pytest.mark.slow
def test_full_training():
    """전체 학습 테스트 (느림)"""
    pass

@pytest.mark.gpu
def test_gpu_computation():
    """GPU가 필요한 테스트"""
    pass

@pytest.mark.skip(reason="API 키가 없어서 건너뜀")
def test_external_api():
    pass

@pytest.mark.skipif(
    not has_gpu(),
    reason="GPU 없음",
)
def test_cuda_operation():
    pass
# 특정 마크만 실행
pytest -m slow
pytest -m "not slow"

# 키워드로 필터링
pytest -k "test_add"
pytest -k "test_add or test_divide"

# 상세 출력
pytest -v

# 첫 번째 실패에서 중단
pytest -x

# 마지막 실패한 테스트만 재실행
pytest --lf

conftest.py - 공유 설정

# tests/conftest.py
# 이 파일의 fixture는 같은 디렉토리와 하위 디렉토리에서 자동으로 사용 가능

import pytest
import numpy as np

@pytest.fixture
def random_seed():
    """재현 가능한 테스트를 위한 랜덤 시드"""
    np.random.seed(42)
    yield 42

@pytest.fixture
def sample_dataset():
    """테스트용 작은 데이터셋"""
    np.random.seed(42)
    X = np.random.randn(100, 10)
    y = (X[:, 0] > 0).astype(int)
    return X, y

@pytest.fixture
def model_config():
    """공통 모델 설정"""
    return {
        "input_dim": 10,
        "hidden_dim": 64,
        "output_dim": 2,
        "dropout": 0.1,
    }

AI/ML에서의 활용

import pytest
import numpy as np

# 1. 데이터 전처리 테스트
def normalize(data: np.ndarray) -> np.ndarray:
    """Min-Max 정규화"""
    min_val = data.min()
    max_val = data.max()
    if min_val == max_val:
        return np.zeros_like(data)
    return (data - min_val) / (max_val - min_val)

def test_normalize_basic():
    data = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
    result = normalize(data)
    assert result.min() == pytest.approx(0.0)
    assert result.max() == pytest.approx(1.0)
    assert len(result) == len(data)

def test_normalize_constant():
    """모든 값이 동일한 경우"""
    data = np.array([5.0, 5.0, 5.0])
    result = normalize(data)
    assert np.all(result == 0.0)

def test_normalize_negative():
    """음수 값 포함"""
    data = np.array([-10.0, 0.0, 10.0])
    result = normalize(data)
    assert result[0] == pytest.approx(0.0)
    assert result[1] == pytest.approx(0.5)
    assert result[2] == pytest.approx(1.0)

# 2. 모델 차원 테스트
def test_model_output_shape(model_config):
    """모델 출력 차원이 올바른지 확인합니다."""
    batch_size = 16
    input_dim = model_config["input_dim"]
    output_dim = model_config["output_dim"]

    # 시뮬레이션 (실제로는 모델 forward)
    X = np.random.randn(batch_size, input_dim)
    output = np.random.randn(batch_size, output_dim)  # 모델 출력 시뮬레이션

    assert output.shape == (batch_size, output_dim)

# 3. 손실 함수 테스트
def mse_loss(predictions: np.ndarray, targets: np.ndarray) -> float:
    """평균 제곱 오차"""
    return float(np.mean((predictions - targets) ** 2))

@pytest.mark.parametrize("pred, target, expected_loss", [
    (np.array([1.0, 2.0, 3.0]), np.array([1.0, 2.0, 3.0]), 0.0),  # 완벽 예측
    (np.array([0.0, 0.0, 0.0]), np.array([1.0, 1.0, 1.0]), 1.0),  # 전부 틀림
])
def test_mse_loss(pred, target, expected_loss):
    loss = mse_loss(pred, target)
    assert loss == pytest.approx(expected_loss)

def test_mse_loss_is_non_negative():
    """MSE 손실은 항상 0 이상이어야 합니다."""
    for _ in range(100):
        pred = np.random.randn(50)
        target = np.random.randn(50)
        assert mse_loss(pred, target) >= 0

# 4. 데이터 분할 테스트
def split_data(data, ratio=0.8):
    """학습/검증 데이터 분할"""
    split_idx = int(len(data) * ratio)
    return data[:split_idx], data[split_idx:]

def test_split_preserves_total():
    """분할 후 데이터 수가 보존되는지 확인"""
    data = list(range(100))
    train, val = split_data(data, ratio=0.8)
    assert len(train) + len(val) == len(data)

def test_split_no_overlap():
    """학습/검증 데이터가 겹치지 않는지 확인"""
    data = list(range(100))
    train, val = split_data(data, ratio=0.8)
    assert set(train).isdisjoint(set(val))

def test_split_ratio():
    """분할 비율이 근사적으로 맞는지 확인"""
    data = list(range(1000))
    train, val = split_data(data, ratio=0.8)
    assert len(train) == pytest.approx(800, abs=5)

pyproject.toml 테스트 설정

# pyproject.toml
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_functions = ["test_*"]
markers = [
    "slow: 느린 테스트 (전체 학습 등)",
    "gpu: GPU가 필요한 테스트",
    "integration: 외부 서비스 통합 테스트",
]
addopts = "-v --tb=short"
unittest는 표준 라이브러리에 포함되어 있지만 클래스 기반이고 코드가 장황합니다. pytest는 간단한 함수와 assert만으로 테스트를 작성할 수 있고, fixture, parametrize, 풍부한 플러그인 생태계를 제공합니다. 새 프로젝트에서는 pytest를 권장합니다.
  1. 데이터 전처리 (정규화, 인코딩, 분할), 2) 모델 입출력 형상 (shape 확인), 3) 손실 함수 (알려진 입력에 대한 출력), 4) 데이터 누수 (학습/검증 분리), 5) 재현성 (시드 고정 시 동일 결과). 전체 학습 테스트는 비용이 크므로 별도 마킹하여 관리합니다.

체크리스트

  • pytest로 테스트 파일을 작성하고 실행할 수 있다
  • assertpytest.approx로 결과를 검증할 수 있다
  • fixture로 테스트 데이터와 리소스를 관리할 수 있다
  • parametrize로 여러 입력에 대한 테스트를 작성할 수 있다
  • ML 코드에서 전처리, 차원, 손실 함수를 테스트할 수 있다

다음 문서