Communitygithub.com

python-testing

pytest 기반 Python 테스트 전략: TDD 방법론, Fixture, Mock, 파라미터화 및 커버리지 요구사항. pytest 테스트 작성 시 활용하세요.

python-testing란 무엇인가요?

이 스킬은 Python 애플리케이션 테스트를 위한 포괄적인 패턴과 모범 사례를 제공합니다. TDD(Test-Driven Development)의 적-녹-리팩터 사이클을 체계적으로 안내하며, pytest 프레임워크를 활용한 단위 테스트, 통합 테스트, 종단간 테스트 작성 방법을 상세히 다룹니다. 주요 기능으로는 Fixture를 이용한 테스트 환경 설정/정리, unittest.mock을 활용한 함수 및 비동기 코드 모킹, pytest.mark.parametrize를 통한 다양한 입력값 테스트, 그리고 pytest-cov를 이용한 코드 커버리지 측정 및 80% 이상 목표 관리가 포함됩니다. 모든 내용은 Claude Code, Cursor, Codex 등 주요 AI 에이전트 플랫폼에서 사용 가능한 마크다운 형식으로 제공됩니다.

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

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

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

문서

Python 테스트 패턴

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

활성화 시점

  • 새로운 Python 코드 작성 시 (TDD: 적, 녹, 리팩터 순서 준수)
  • 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():
    """리스트 추가 테스트."""
    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("오류 메시지")

# 예외 메시지 확인
with pytest.raises(ValueError, match="잘못된 입력"):
    raise ValueError("잘못된 입력입니다")

# 예외 속성 확인
with pytest.raises(ValueError) as exc_info:
    raise ValueError("오류 메시지")
assert str(exc_info.value) == "오류 메시지"

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

Setup/Teardown이 있는 Fixture

@pytest.fixture
def database():
    """Setup과 Teardown이 있는 Fixture."""
    # Setup
    db = Database(":memory:")
    db.create_tables()
    db.insert_test_data()

    yield db  # 테스트에 제공

    # Teardown
    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)

Autouse Fixture

@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=["유효-이메일", "at-누락", "도메인-누락"])
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"

# 단위 테스트이면서 느리지 않은 테스트 실행
pytest -m "unit and not slow"

pytest.ini에서 마커 설정

[pytest]
markers =
    slow: 느린 테스트를 표시합니다
    integration: 통합 테스트를 표시합니다
    unit: 단위 테스트를 표시합니다
    django: Django가 필요한 테스트를 표시합니다

모킹과 패칭

함수 모킹

from unittest.mock import patch, Mock

@patch("mypackage.external_api_call")
def test_with_mock(api_call_mock):
    """모의 외부 API로 테스트."""
    api_call_mock.return_value = {"status": "success"}

    result = my_function()

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

반환값 모킹

@patch("mypackage.Database.connect")
def test_database_connection(connect_mock):
    """모의 데이터베이스 연결로 테스트."""
    connect_mock.return_value = MockConnection()

    db = Database()
    db.connect()

    connect_mock.assert_called_once_with("localhost")

예외 모킹

@patch("mypackage.api_call")
def test_api_error_handling(api_call_mock):
    """모의 예외로 오류 처리 테스트."""
    api_call_mock.side_effect = ConnectionError("네트워크 오류")

    with pytest.raises(ConnectionError):
        api_call()

    api_call_mock.assert_called_once()

컨텍스트 관리자 모킹

@patch("builtins.open", new_callable=mock_open)
def test_file_reading(mock_file):
    """모의 open으로 파일 읽기 테스트."""
    mock_file.return_value.read.return_value = "파일 내용"

    result = read_file("test.txt")

    mock_file.assert_called_once_with("test.txt", "r")
    assert result == "파일 내용"

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()

클래스 인스턴스 모킹

class TestUserService:
    @patch("mypackage.UserRepository")
    def test_create_user(self, repo_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()

속성(Property) 모킹

@pytest.fixture
def mock_config():
    """속성을 가진 모의 객체 생성."""
    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):
    """모의 설정 속성으로 테스트."""
    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

비동기 함수 모킹

@pytest.mark.asyncio
@patch("mypackage.async_api_call")
async def test_async_mock(api_call_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="잘못된 입력"):
        validate_input("잘못된 입력")

예외 속성 테스트

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

    assert exc_info.value.code == 400
    assert "오류" 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("테스트 내용")
        temp_path = f.name

    try:
        result = process_file(temp_path)
        assert result == "처리됨: 테스트 내용"
    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):
        """이 클래스의 각 테스트 전에 실행되는 Setup."""
        self.service = UserService()

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

    def test_delete_user(self):
        """사용자 삭제 테스트."""
        self.service.create_user("Alice")
        result = self.service.delete_user(1)
        assert result is True

선택적 테스트 건너뛰기

import sys
import pytest

# 조건부 건너뛰기
@pytest.mark.skipif(sys.version_info < (3, 10), reason="Python 3.10+ 필요")
def test_new_feature():
    from mypackage import new_feature

# 무조건 건너뛰기
@pytest.mark.skip(reason="아직 구현되지 않음")
def test_future_feature():
    pass

# 예상 실패
@pytest.mark.xfail(reason="알려진 버그 #1234")
def test_known_bug():
    result = buggy_function()
    assert result == expected

고급 테스트 기법

플러그인을 사용한 pytest

# 추가 유용한 플러그인 설치
pip install pytest-xdist pytest-cov pytest-mock pytest-timeout

# 병렬 실행
pytest -n auto

# 시간 초과 설정
pytest --timeout=30

테스트 시간 초과

@pytest.mark.timeout(5)
def test_quick_operation():
    """5초 초과시 실패하는 테스트."""
    result = quick_operation()
    assert result is not None

리소스 누수 테스트

def test_database_connection_leak(database):
    """데이터베이스 연결 누수 테스트."""
    initial_count = database.connection_count()
    
    for _ in range(100):
        conn = database.get_connection()
        conn.close()
    
    final_count = database.connection_count()
    assert final_count == initial_count

네트워크 요청 모킹 (pytest-mock 사용)

# conftest.py
def pytest_configure(config):
    """네트워크 요청을 자동으로 차단합니다."""
    config.addinivalue_line(
        "markers", "e2e: 종단간 테스트 (네트워크 요청 허용)"
    )

@pytest.fixture(autouse=True)
def mock_network_requests(mocker):
    """모든 네트워크 요청을 차단하는 Fixture."""
    mocker.patch("requests.get")
    mocker.patch("requests.post")
    mocker.patch("requests.put")
    mocker.patch("requests.delete")

환경 설정 권장 사항

pyproject.toml

[tool.pytest.ini_options]
minversion = "7.0"
addopts = "-ra -q --strict-markers"
testpaths = [
    "tests",
]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
markers = [
    "slow: 느린 테스트",
    "integration: 통합 테스트",
    "unit: 단위 테스트",
    "e2e: 종단간 테스트",
    "django: Django 테스트",
]

setup.cfg

[tool:pytest]
minversion = 7.0
addopts = -ra -q --strict-markers
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
markers =
    slow: 느린 테스트
    integration: 통합 테스트
    unit: 단위 테스트
    e2e: 종단간 테스트
    django: Django 테스트

추가 리소스

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/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

affaan-m/ECC

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

affaan-m/fal-ai-media

Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.

affaan-m/manim-video

日本語翻訳:このファイルは manim-video 用の日本語翻訳が必要です

affaan-m/remotion-video-creation

Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。

affaan-m/video-editing

AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.

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.

agent-self-evaluation

Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.

agent-sort

Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

ai-first-engineering

Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.

ai-regression-testing

Regression testing strategies for AI-assisted development. Sandbox-mode API testing without database dependencies, automated bug-check workflows, and patterns to catch AI blind spots where the same model writes and reviews code. Use when adding regression coverage to AI-assisted code, or when the same model both wrote and reviewed a change.

android-clean-architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

angular-developer

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.

관련 스킬