pytest-coder

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Pytest Coder

Pytest 测试编码指南

Core Philosophy

核心原则

PrincipleApplication
AAA PatternArrange-Act-Assert for every test
Behavior over ImplementationTest what code does, not how
IsolationTests must be independent
Fast TestsMock I/O, minimize database hits
Descriptive NamesTest name explains the scenario
CoverageTest happy paths AND edge cases
原则应用场景
AAA Pattern每个测试遵循Arrange-Act-Assert(准备-执行-断言)模式
Behavior over Implementation测试代码的行为,而非实现细节
Isolation测试必须相互独立
Fast TestsMock I/O操作,减少数据库访问
Descriptive Names测试名称需清晰说明场景
Coverage测试正常路径及边缘情况

Project Structure

项目结构

tests/
├── conftest.py          # Shared fixtures
├── unit/                # Unit tests (fast, isolated)
│   ├── test_models.py
│   └── test_services.py
├── integration/         # Integration tests (real dependencies)
│   └── test_api.py
└── fixtures/            # Test data files
    └── sample_data.json
tests/
├── conftest.py          # Shared fixtures
├── unit/                # Unit tests (fast, isolated)
│   ├── test_models.py
│   └── test_services.py
├── integration/         # Integration tests (real dependencies)
│   └── test_api.py
└── fixtures/            # Test data files
    └── sample_data.json

Essential Patterns

核心模式

Basic Test Structure

基础测试结构

python
import pytest
from myapp.services import UserService

class TestUserService:
    """Tests for UserService."""

    def test_create_user_with_valid_data(self, user_service):
        # Arrange
        user_data = {"email": "test@example.com", "name": "Test User"}

        # Act
        result = user_service.create(user_data)

        # Assert
        assert result.email == "test@example.com"
        assert result.id is not None

    def test_create_user_with_duplicate_email_raises_error(self, user_service, existing_user):
        # Arrange
        user_data = {"email": existing_user.email, "name": "Another User"}

        # Act & Assert
        with pytest.raises(ValueError, match="Email already exists"):
            user_service.create(user_data)
python
import pytest
from myapp.services import UserService

class TestUserService:
    """Tests for UserService."""

    def test_create_user_with_valid_data(self, user_service):
        # Arrange
        user_data = {"email": "test@example.com", "name": "Test User"}

        # Act
        result = user_service.create(user_data)

        # Assert
        assert result.email == "test@example.com"
        assert result.id is not None

    def test_create_user_with_duplicate_email_raises_error(self, user_service, existing_user):
        # Arrange
        user_data = {"email": existing_user.email, "name": "Another User"}

        # Act & Assert
        with pytest.raises(ValueError, match="Email already exists"):
            user_service.create(user_data)

Fixtures

Fixture 用法

python
undefined
python
undefined

conftest.py

conftest.py

import pytest from myapp.database import get_db from myapp.services import UserService
@pytest.fixture def db(): """Provide a clean database session.""" session = get_db() yield session session.rollback()
@pytest.fixture def user_service(db): """Provide UserService instance.""" return UserService(db)
@pytest.fixture def sample_user(): """Provide sample user data.""" return {"email": "test@example.com", "name": "Test User", "password": "secret123"}
@pytest.fixture def existing_user(db, sample_user): """Create and return an existing user.""" from myapp.models import User user = User(**sample_user) db.add(user) db.commit() return user
undefined
import pytest from myapp.database import get_db from myapp.services import UserService
@pytest.fixture def db(): """Provide a clean database session.""" session = get_db() yield session session.rollback()
@pytest.fixture def user_service(db): """Provide UserService instance.""" return UserService(db)
@pytest.fixture def sample_user(): """Provide sample user data.""" return {"email": "test@example.com", "name": "Test User", "password": "secret123"}
@pytest.fixture def existing_user(db, sample_user): """Create and return an existing user.""" from myapp.models import User user = User(**sample_user) db.add(user) db.commit() return user
undefined

Parametrized Tests

参数化测试

python
import pytest

@pytest.mark.parametrize("input_email,expected_valid", [
    ("valid@example.com", True),
    ("also.valid@domain.co.uk", True),
    ("invalid-email", False),
    ("missing@domain", False),
    ("", False),
])
def test_email_validation(input_email, expected_valid):
    from myapp.validators import is_valid_email
    assert is_valid_email(input_email) == expected_valid

@pytest.mark.parametrize("status,expected_message", [
    ("pending", "Order is being processed"),
    ("shipped", "Order has been shipped"),
    ("delivered", "Order has been delivered"),
], ids=["pending-status", "shipped-status", "delivered-status"])
def test_order_status_message(status, expected_message):
    from myapp.orders import get_status_message
    assert get_status_message(status) == expected_message
python
import pytest

@pytest.mark.parametrize("input_email,expected_valid", [
    ("valid@example.com", True),
    ("also.valid@domain.co.uk", True),
    ("invalid-email", False),
    ("missing@domain", False),
    ("", False),
])
def test_email_validation(input_email, expected_valid):
    from myapp.validators import is_valid_email
    assert is_valid_email(input_email) == expected_valid

@pytest.mark.parametrize("status,expected_message", [
    ("pending", "Order is being processed"),
    ("shipped", "Order has been shipped"),
    ("delivered", "Order has been delivered"),
], ids=["pending-status", "shipped-status", "delivered-status"])
def test_order_status_message(status, expected_message):
    from myapp.orders import get_status_message
    assert get_status_message(status) == expected_message

Mocking

Mocking 实践

python
from unittest.mock import Mock, patch, AsyncMock

def test_send_email_calls_smtp(user_service):
    # Mock external dependency
    with patch("myapp.services.smtp_client") as mock_smtp:
        mock_smtp.send.return_value = True

        user_service.send_welcome_email("test@example.com")

        mock_smtp.send.assert_called_once_with(
            to="test@example.com",
            subject="Welcome!",
        )

def test_payment_processing_handles_failure():
    mock_gateway = Mock()
    mock_gateway.charge.side_effect = PaymentError("Card declined")

    service = PaymentService(gateway=mock_gateway)

    with pytest.raises(PaymentError):
        service.process_payment(amount=100)
python
from unittest.mock import Mock, patch, AsyncMock

def test_send_email_calls_smtp(user_service):
    # Mock external dependency
    with patch("myapp.services.smtp_client") as mock_smtp:
        mock_smtp.send.return_value = True

        user_service.send_welcome_email("test@example.com")

        mock_smtp.send.assert_called_once_with(
            to="test@example.com",
            subject="Welcome!",
        )

def test_payment_processing_handles_failure():
    mock_gateway = Mock()
    mock_gateway.charge.side_effect = PaymentError("Card declined")

    service = PaymentService(gateway=mock_gateway)

    with pytest.raises(PaymentError):
        service.process_payment(amount=100)

Async Testing

异步测试

python
import pytest

@pytest.mark.asyncio
async def test_async_fetch_user(user_service):
    # Arrange
    user_id = 1

    # Act
    user = await user_service.get_by_id(user_id)

    # Assert
    assert user.id == user_id

@pytest.fixture
async def async_db():
    """Async database session fixture."""
    from myapp.database import async_session
    async with async_session() as session:
        yield session
        await session.rollback()
python
import pytest

@pytest.mark.asyncio
async def test_async_fetch_user(user_service):
    # Arrange
    user_id = 1

    # Act
    user = await user_service.get_by_id(user_id)

    # Assert
    assert user.id == user_id

@pytest.fixture
async def async_db():
    """Async database session fixture."""
    from myapp.database import async_session
    async with async_session() as session:
        yield session
        await session.rollback()

Mock async functions

Mock async functions

@pytest.mark.asyncio async def test_async_external_api(): with patch("myapp.client.fetch_data", new_callable=AsyncMock) as mock_fetch: mock_fetch.return_value = {"status": "ok"}
    result = await fetch_and_process()

    assert result["status"] == "ok"
undefined
@pytest.mark.asyncio async def test_async_external_api(): with patch("myapp.client.fetch_data", new_callable=AsyncMock) as mock_fetch: mock_fetch.return_value = {"status": "ok"}
    result = await fetch_and_process()

    assert result["status"] == "ok"
undefined

Testing Exceptions

异常测试

python
import pytest

def test_divide_by_zero_raises_error():
    with pytest.raises(ZeroDivisionError):
        divide(10, 0)

def test_invalid_input_raises_with_message():
    with pytest.raises(ValueError, match="must be positive"):
        process_amount(-100)

def test_exception_attributes():
    with pytest.raises(CustomError) as exc_info:
        risky_operation()

    assert exc_info.value.code == "E001"
    assert "failed" in str(exc_info.value)
python
import pytest

def test_divide_by_zero_raises_error():
    with pytest.raises(ZeroDivisionError):
        divide(10, 0)

def test_invalid_input_raises_with_message():
    with pytest.raises(ValueError, match="must be positive"):
        process_amount(-100)

def test_exception_attributes():
    with pytest.raises(CustomError) as exc_info:
        risky_operation()

    assert exc_info.value.code == "E001"
    assert "failed" in str(exc_info.value)

Fixture Scopes

Fixture 作用域

ScopeLifecycleUse Case
function
Per test (default)Most fixtures
class
Per test classShared setup within class
module
Per moduleExpensive setup shared by module
session
Entire test runDatabase connections, servers
python
@pytest.fixture(scope="session")
def database_engine():
    """Create engine once for entire test session."""
    engine = create_engine(TEST_DATABASE_URL)
    yield engine
    engine.dispose()

@pytest.fixture(scope="function")
def db_session(database_engine):
    """Create fresh session per test."""
    connection = database_engine.connect()
    transaction = connection.begin()
    session = Session(bind=connection)

    yield session

    session.close()
    transaction.rollback()
    connection.close()
作用域生命周期适用场景
function
每个测试用例(默认)大多数fixture
class
每个测试类测试类内的共享初始化
module
每个模块模块内共享的耗时初始化操作
session
整个测试运行周期数据库连接、服务器等
python
@pytest.fixture(scope="session")
def database_engine():
    """Create engine once for entire test session."""
    engine = create_engine(TEST_DATABASE_URL)
    yield engine
    engine.dispose()

@pytest.fixture(scope="function")
def db_session(database_engine):
    """Create fresh session per test."""
    connection = database_engine.connect()
    transaction = connection.begin()
    session = Session(bind=connection)

    yield session

    session.close()
    transaction.rollback()
    connection.close()

Markers

标记用法

python
undefined
python
undefined

pytest.ini or pyproject.toml

pytest.ini or pyproject.toml

[tool.pytest.ini_options] markers = [ "slow: marks tests as slow", "integration: marks integration tests", "unit: marks unit tests", ]
[tool.pytest.ini_options] markers = [ "slow: marks tests as slow", "integration: marks integration tests", "unit: marks unit tests", ]

Usage

Usage

@pytest.mark.slow def test_complex_calculation(): ...
@pytest.mark.integration def test_database_connection(): ...
@pytest.mark.slow def test_complex_calculation(): ...
@pytest.mark.integration def test_database_connection(): ...

Run specific markers

Run specific markers

pytest -m "not slow"

pytest -m "not slow"

pytest -m "unit"

pytest -m "unit"

undefined
undefined

Quality Checklist

质量检查清单

  • AAA pattern (Arrange-Act-Assert) in every test
  • Descriptive test names explaining the scenario
  • Fixtures for common setup
  • Parametrized tests for multiple inputs
  • Mocks for external dependencies
  • Happy path tested
  • Error cases tested
  • Edge cases covered
  • Async tests use
    @pytest.mark.asyncio
  • No test interdependencies
  • Coverage >90%
  • 每个测试遵循AAA(准备-执行-断言)模式
  • 测试名称清晰说明场景
  • 使用fixture处理通用初始化
  • 对多输入场景使用参数化测试
  • 对外部依赖使用Mock
  • 已测试正常路径
  • 已测试错误场景
  • 已覆盖边缘情况
  • 异步测试使用
    @pytest.mark.asyncio
  • 测试之间无依赖
  • 测试覆盖率>90%

Anti-Patterns

反模式

Anti-PatternWhy BadFix
Tests depend on orderFlaky, hard to debugUse fixtures, isolate
Testing implementationBrittle testsTest behavior
Too many assertionsHard to identify failureOne assertion per test
No error case testsMissing coverageTest exceptions explicitly
Slow unit testsSlow feedbackMock I/O, use in-memory DB
反模式问题所在修复方案
测试依赖执行顺序测试不稳定,难以调试使用fixture,确保测试独立
测试实现细节测试脆弱,易随代码修改失效测试行为而非实现
单个测试包含过多断言难以定位失败原因每个测试仅保留一个断言
未测试错误场景覆盖率不足显式测试异常情况
单元测试速度慢反馈周期长Mock I/O操作,使用内存数据库