Communitygithub.com

python-testing

pytest를 활용한 Python 테스트 전략 — TDD 방법론, Fixture, Mock, 매개변수화, 커버리지 요구사항 포함. pytest 테스트 작성 시 (Fixture, Mock, 매개변수화, 커버리지) 사용합니다.

python-testing란 무엇인가요?

이 스킬은 Python 애플리케이션을 위한 체계적인 테스트 전략을 제공합니다. TDD(Test-Driven Development) 방법론을 기반으로, Red-Green-Refactor 사이클을 따라 신뢰성 높은 코드를 작성하도록 돕습니다. pytest의 핵심 기능인 Fixture(설정/정리, 범위, 매개변수화, 자동 사용, conftest.py 공유), Mock(함수, 예외, 컨텍스트 관리자, autospec, 속성), 매개변수화, 마커 사용자 정의 및 테스트 선택, 비동기 코드 테스트(pytest-asyncio), 예외 테스트, 파일/임시 디렉토리 테스트까지 포괄합니다. 또한 80% 이상의 코드 커버리지 목표와 중요 경로 100% 커버리지 요구사항을 명시하며, pytest-cov 플러그인을 통한 측정 방법을 안내합니다. 이 스킬은 Claude Code, Cursor, Codex 등 AI 에이전트가 Python 프로젝트의 테스트 인프라를 설계하거나 테스트 코드를 작성할 때 참고할 수 있는 실용적인 패턴과 예제로 구성되어 있습니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/everything-claude-code/tree/main/skills/python-testing

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

Python 테스트 패턴

pytest, TDD 방법론 및 모범 사례를 사용한 Python 애플리케이션을 위한 포괄적인 테스트 전략.

활성화 시점

  • 새로운 Python 코드를 작성할 때 (TDD 준수: red, green, refactor)
  • Python 프로젝트의 테스트 스위트를 설계할 때
  • Python 테스트 커버리지를 검토할 때
  • 테스트 인프라를 설정할 때

핵심 테스트 철학

테스트 주도 개발 (TDD)

항상 TDD 사이클을 따르십시오:

  1. RED: 원하는 동작에 대한 실패하는 테스트를 작성합니다.
  2. GREEN: 테스트를 통과시키는 최소한의 코드를 작성합니다.
  3. REFACTOR: 테스트가 통과하는 상태를 유지하면서 코드를 개선합니다.
# 1단계: 실패하는 테스트 작성 (RED)
def test_add_numbers():
    result = add(2, 3)
    assert result == 5

# 2단계: 최소 구현 작성 (GREEN)
def add(a, b):
    return a + b

# 3단계: 필요한 경우 리팩터링 (REFACTOR)

커버리지 요구사항

  • 목표: 80% 이상의 코드 커버리지
  • 중요 경로: 100% 커버리지 필요
  • 커버리지 측정에는 pytest --cov 사용
pytest --cov=mypackage --cov-report=term-missing --cov-report=html

pytest 기본

기본 테스트 구조

import pytest

def test_addition():
    """기본 덧셈 테스트."""
    assert 2 + 2 == 4

def test_string_uppercase():
    """문자열 대문자 변환 테스트."""
    text = "hello"
    assert text.upper() == "HELLO"

def test_list_append():
    """리스트 append 테스트."""
    items = [1, 2, 3]
    items.append(4)
    assert 4 in items
    assert len(items) == 4

단언문 (Assertions)

# 동등성
assert result == expected

# 부등성
assert result != unexpected

# 진리값
assert result  # 참 같은 값
assert not result  # 거짓 같은 값
assert result is True  # 정확히 True
assert result is False  # 정확히 False
assert result is None  # 정확히 None

# 포함 관계
assert item in collection
assert item not in collection

# 비교
assert result > 0
assert 0 <= result <= 100

# 타입 확인
assert isinstance(result, str)

# 예외 테스트 (권장 방식)
with pytest.raises(ValueError):
    raise ValueError("error message")

# 예외 메시지 확인
with pytest.raises(ValueError, match="invalid input"):
    raise ValueError("invalid input provided")

# 예외 속성 확인
with pytest.raises(ValueError) as exc_info:
    raise ValueError("error message")
assert str(exc_info.value) == "error message"

Fixture

기본 Fixture 사용법

import pytest

@pytest.fixture
def sample_data():
    """샘플 데이터를 제공하는 Fixture."""
    return {"name": "Alice", "age": 30}

def test_sample_data(sample_data):
    """Fixture를 사용한 테스트."""
    assert sample_data["name"] == "Alice"
    assert sample_data["age"] == 30

설정/정리가 있는 Fixture

@pytest.fixture
def database():
    """설정 및 정리가 있는 Fixture."""
    # 설정
    db = Database(":memory:")
    db.create_tables()
    db.insert_test_data()

    yield db  # 테스트에 제공

    # 정리
    db.close()

def test_database_query(database):
    """데이터베이스 연산 테스트."""
    result = database.query("SELECT * FROM users")
    assert len(result) > 0

Fixture 범위 (Scope)

# 함수 범위 (기본값) - 각 테스트마다 실행
@pytest.fixture
def temp_file():
    with open("temp.txt", "w") as f:
        yield f
    os.remove("temp.txt")

# 모듈 범위 - 모듈당 한 번 실행
@pytest.fixture(scope="module")
def module_db():
    db = Database(":memory:")
    db.create_tables()
    yield db
    db.close()

# 세션 범위 - 테스트 세션당 한 번 실행
@pytest.fixture(scope="session")
def shared_resource():
    resource = ExpensiveResource()
    yield resource
    resource.cleanup()

매개변수화된 Fixture

@pytest.fixture(params=[1, 2, 3])
def number(request):
    """매개변수화된 Fixture."""
    return request.param

def test_numbers(number):
    """각 매개변수에 대해 한 번씩, 총 3번 실행되는 테스트."""
    assert number > 0

여러 Fixture 사용

@pytest.fixture
def user():
    return User(id=1, name="Alice")

@pytest.fixture
def admin():
    return User(id=2, name="Admin", role="admin")

def test_user_admin_interaction(user, admin):
    """여러 Fixture를 사용한 테스트."""
    assert admin.can_manage(user)

자동 사용 Fixture (Autouse)

@pytest.fixture(autouse=True)
def reset_config():
    """모든 테스트 전에 자동으로 실행됩니다."""
    Config.reset()
    yield
    Config.cleanup()

def test_without_fixture_call():
    # reset_config가 자동으로 실행됨
    assert Config.get_setting("debug") is False

공유 Fixture를 위한 Conftest.py

# tests/conftest.py
import pytest

@pytest.fixture
def client():
    """모든 테스트를 위한 공유 Fixture."""
    app = create_app(testing=True)
    with app.test_client() as client:
        yield client

@pytest.fixture
def auth_headers(client):
    """API 테스트를 위한 인증 헤더 생성."""
    response = client.post("/api/login", json={
        "username": "test",
        "password": "test"
    })
    token = response.json["token"]
    return {"Authorization": f"Bearer {token}"}

매개변수화 (Parametrization)

기본 매개변수화

@pytest.mark.parametrize("input,expected", [
    ("hello", "HELLO"),
    ("world", "WORLD"),
    ("PyThOn", "PYTHON"),
])
def test_uppercase(input, expected):
    """다양한 입력으로 3번 실행되는 테스트."""
    assert input.upper() == expected

여러 매개변수

@pytest.mark.parametrize("a,b,expected", [
    (2, 3, 5),
    (0, 0, 0),
    (-1, 1, 0),
    (100, 200, 300),
])
def test_add(a, b, expected):
    """여러 입력으로 덧셈 테스트."""
    assert add(a, b) == expected

ID를 사용한 매개변수화

@pytest.mark.parametrize("input,expected", [
    ("[email protected]", True),
    ("invalid", False),
    ("@no-domain.com", False),
], ids=["valid-email", "missing-at", "missing-domain"])
def test_email_validation(input, expected):
    """읽기 가능한 테스트 ID로 이메일 검증 테스트."""
    assert is_valid_email(input) is expected

매개변수화된 Fixture

@pytest.fixture(params=["sqlite", "postgresql", "mysql"])
def db(request):
    """여러 데이터베이스 백엔드에 대해 테스트."""
    if request.param == "sqlite":
        return Database(":memory:")
    elif request.param == "postgresql":
        return Database("postgresql://localhost/test")
    elif request.param == "mysql":
        return Database("mysql://localhost/test")

def test_database_operations(db):
    """각 데이터베이스에 대해 한 번씩, 총 3번 실행되는 테스트."""
    result = db.query("SELECT 1")
    assert result is not None

마커와 테스트 선택

사용자 정의 마커

# 느린 테스트 표시
@pytest.mark.slow
def test_slow_operation():
    time.sleep(5)

# 통합 테스트 표시
@pytest.mark.integration
def test_api_integration():
    response = requests.get("https://api.example.com")
    assert response.status_code == 200

# 단위 테스트 표시
@pytest.mark.unit
def test_unit_logic():
    assert calculate(2, 3) == 5

특정 테스트 실행

# 빠른 테스트만 실행
pytest -m "not slow"

# 통합 테스트만 실행
pytest -m integration

# 통합 또는 느린 테스트 실행
pytest -m "integration or slow"

# unit으로 표시되었지만 slow는 아닌 테스트 실행
pytest -m "unit and not slow"

pytest.ini에서 마커 설정

[pytest]
markers =
    slow: marks tests as slow
    integration: marks tests as integration tests
    unit: marks tests as unit tests
    django: marks tests as requiring Django

Mock과 Patching

함수 Mocking

from unittest.mock import patch, Mock

@patch("mypackage.external_api_call")
def test_with_mock(api_call_mock):
    """외부 API를 Mock으로 대체한 테스트."""
    api_call_mock.return_value = {"status": "success"}

    result = my_function()

    api_call_mock.assert_called_once()
    assert result["status"] == "success"

반환 값 Mocking

@patch("mypackage.Database.connect")
def test_database_connection(connect_mock):
    """데이터베이스 연결을 Mock으로 대체한 테스트."""
    connect_mock.return_value = MockConnection()

    db = Database()
    db.connect()

    connect_mock.assert_called_once_with("localhost")

예외 Mocking

@patch("mypackage.api_call")
def test_api_error_handling(api_call_mock):
    """Mock 예외를 사용한 오류 처리 테스트."""
    api_call_mock.side_effect = ConnectionError("Network error")

    with pytest.raises(ConnectionError):
        api_call()

    api_call_mock.assert_called_once()

컨텍스트 관리자 Mocking

@patch("builtins.open", new_callable=mock_open)
def test_file_reading(mock_file):
    """open을 Mock으로 대체한 파일 읽기 테스트."""
    mock_file.return_value.read.return_value = "file content"

    result = read_file("test.txt")

    mock_file.assert_called_once_with("test.txt", "r")
    assert result == "file content"

Autospec 사용

@patch("mypackage.DBConnection", autospec=True)
def test_autospec(db_mock):
    """Autospec을 사용하여 API 오용을 잡는 테스트."""
    db = db_mock.return_value
    db.query("SELECT * FROM users")

    # DBConnection에 query 메서드가 없으면 실패함
    db_mock.assert_called_once()

클래스 인스턴스 Mock

class TestUserService:
    @patch("mypackage.UserRepository")
    def test_create_user(self, repo_mock):
        """저장소를 Mock으로 대체한 사용자 생성 테스트."""
        repo_mock.return_value.save.return_value = User(id=1, name="Alice")

        service = UserService(repo_mock.return_value)
        user = service.create_user(name="Alice")

        assert user.name == "Alice"
        repo_mock.return_value.save.assert_called_once()

속성 Mock

@pytest.fixture
def mock_config():
    """속성을 가진 Mock 생성."""
    config = Mock()
    type(config).debug = PropertyMock(return_value=True)
    type(config).api_key = PropertyMock(return_value="test-key")
    return config

def test_with_mock_config(mock_config):
    """Mock된 설정 속성 테스트."""
    assert mock_config.debug is True
    assert mock_config.api_key == "test-key"

비동기 코드 테스트

pytest-asyncio를 사용한 비동기 테스트

import pytest

@pytest.mark.asyncio
async def test_async_function():
    """비동기 함수 테스트."""
    result = await async_add(2, 3)
    assert result == 5

@pytest.mark.asyncio
async def test_async_with_fixture(async_client):
    """비동기 Fixture를 사용한 비동기 테스트."""
    response = await async_client.get("/api/users")
    assert response.status_code == 200

비동기 Fixture

@pytest.fixture
async def async_client():
    """비동기 테스트 클라이언트를 제공하는 비동기 Fixture."""
    app = create_app()
    async with app.test_client() as client:
        yield client

@pytest.mark.asyncio
async def test_api_endpoint(async_client):
    """비동기 Fixture를 사용한 테스트."""
    response = await async_client.get("/api/data")
    assert response.status_code == 200

비동기 함수 Mocking

@pytest.mark.asyncio
@patch("mypackage.async_api_call")
async def test_async_mock(api_call_mock):
    """Mock을 사용한 비동기 함수 테스트."""
    api_call_mock.return_value = {"status": "ok"}

    result = await my_async_function()

    api_call_mock.assert_awaited_once()
    assert result["status"] == "ok"

예외 테스트

예상 예외 테스트

def test_divide_by_zero():
    """0으로 나누면 ZeroDivisionError가 발생하는지 테스트."""
    with pytest.raises(ZeroDivisionError):
        divide(10, 0)

def test_custom_exception():
    """메시지가 있는 사용자 정의 예외 테스트."""
    with pytest.raises(ValueError, match="invalid input"):
        validate_input("invalid")

예외 속성 테스트

def test_exception_with_details():
    """사용자 정의 속성을 가진 예외 테스트."""
    with pytest.raises(CustomError) as exc_info:
        raise CustomError("error", code=400)

    assert exc_info.value.code == 400
    assert "error" in str(exc_info.value)

부수 효과 테스트

파일 연산 테스트

import tempfile
import os

def test_file_processing():
    """임시 파일로 파일 처리 테스트."""
    with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt') as f:
        f.write("test content")
        temp_path = f.name

    try:
        result = process_file(temp_path)
        assert result == "processed: test content"
    finally:
        os.unlink(temp_path)

pytest의 tmp_path Fixture 사용

def test_with_tmp_path(tmp_path):
    """pytest 내장 임시 경로 Fixture 사용 테스트."""
    test_file = tmp_path / "test.txt"
    test_file.write_text("hello world")

    result = process_file(str(test_file))
    assert result == "hello world"
    # tmp_path는 자동으로 정리됨

tmpdir Fixture 사용

def test_with_tmpdir(tmpdir):
    """pytest의 tmpdir Fixture 사용 테스트."""
    test_file = tmpdir.join("test.txt")
    test_file.write("data")

    result = process_file(str(test_file))
    assert result == "data"

테스트 조직

디렉토리 구조

tests/
├── conftest.py                 # 공유 Fixture
├── __init__.py
├── unit/                       # 단위 테스트
│   ├── __init__.py
│   ├── test_models.py
│   ├── test_utils.py
│   └── test_services.py
├── integration/                # 통합 테스트
│   ├── __init__.py
│   ├── test_api.py
│   └── test_database.py
└── e2e/                        # 종단간 테스트
    ├── __init__.py
    └── test_user_flow.py

테스트 클래스

class TestUserService:
    """관련 테스트를 클래스로 그룹화."""

    @pytest.fixture(autouse=True)
    def setup(self):
        """이 클래스의 각 테스트 전에 실행되는 설정."""
        self.service = UserService()

    def test_create_user(self):
        """사용자 생성 테스트."""
        user = self.service.create_user("Alice")
        assert user.name == "Alice"

    def test_delete_user(self):
        """사용자 삭제 테스트."""
        user = self.service.create_user("Bob")
        assert self.service.delete_user(user.id) is True

pytest 구성

pytest.ini

[pytest]
minversion = 7.0
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
markers =
    slow: 느리게 실행되는 테스트
    integration: 통합 테스트
    unit: 단위 테스트
    asyncio: 비동기 테스트

pyproject.toml (대체 구성)

[tool.pytest.ini_options]
minversion = "7.0"
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
markers = [
    "slow: 느리게 실행되는 테스트",
    "integration: 통합 테스트",
    "unit: 단위 테스트",
]

conftest.py에서의 옵션

# tests/conftest.py
def pytest_addoption(parser):
    """사용자 정의 명령줄 옵션 추가."""
    parser.addoption(
        "--runslow", action="store_true", default=False, help="느린 테스트 실행"
    )


def pytest_configure(config):
    """skipif 마커 추가."""
    config.addinivalue_line(
        "markers", "slow: 느리게 실행되는 테스트 (기본적으로 건너뜀)"
    )


def pytest_collection_modifyitems(config, items):
    """--runslow가 제공되지 않으면 느린 테스트 건너뛰기."""
    if not config.getoption("--runslow"):
        skip_slow = pytest.mark.skip(reason="--runslow 옵션 필요")
        for item in items:
            if "slow" in item.keywords:
                item.add_marker(skip_slow)

권장 플러그인

필수

# 기본
pip install pytest

# 커버리지
pip install pytest-cov

# 비동기 테스트
pip install pytest-asyncio

유용한 선택 사항

# 출력 포매팅 개선
pip install pytest-sugar

# xdist로 병렬 테스트 실행
pip install pytest-xdist

# 예쁜 실패 메시지
pip install pytest-clarity

# 무작위 테스트 순서
pip install pytest-randomly

# 고급 Mock 지원
pip install pytest-mock

요약 모범 사례

  1. TDD 준수: Red → Green → Refactor 주기 따르기
  2. 명확한 테스트 이름: test_<기능>_<상태>_<예상결과> 패턴 사용
  3. 하나의 테스트는 하나의 동작만 검증: 여러 조건을 한 테스트에 혼합하지 않기
  4. 적절한 Fixture 사용: 중복을 방지하고 가독성 향상
  5. 공유 Fixture는 conftest.py에 배치: 테스트 계층 전반에 걸쳐 재사용
  6. 테스트를 격리하라: 각 테스트는 독립적으로 실행되어야 함
  7. 너무 많은 Mock을 피하라: 필요할 때만 Mock을 사용; 과도한 Mock은 테스트의 가치를 떨어뜨림
  8. 가능한 한 매개변수화를 사용하라: 다양한 입력을 테스트하기 위해
  9. 코드 커버리지를 측정하되 집착하지 마라: 80% 이상을 목표로 하되, 중요한 경로에 집중
  10. 테스트를 문서처럼 취급하라: 테스트는 코드가 어떻게 동작해야 하는지를 설명해야 함

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

관련 스킬