Loading...
Loading...
Compare original and translation side by side
| Principle | Application |
|---|---|
| AAA Pattern | Arrange-Act-Assert for every test |
| Behavior over Implementation | Test what code does, not how |
| Isolation | Tests must be independent |
| Fast Tests | Mock I/O, minimize database hits |
| Descriptive Names | Test name explains the scenario |
| Coverage | Test happy paths AND edge cases |
| 原则 | 应用场景 |
|---|---|
| AAA Pattern | 每个测试遵循Arrange-Act-Assert(准备-执行-断言)模式 |
| Behavior over Implementation | 测试代码的行为,而非实现细节 |
| Isolation | 测试必须相互独立 |
| Fast Tests | Mock I/O操作,减少数据库访问 |
| Descriptive Names | 测试名称需清晰说明场景 |
| Coverage | 测试正常路径及边缘情况 |
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.jsontests/
├── 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.jsonimport 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)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)undefinedundefinedundefinedundefinedimport 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_messageimport 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_messagefrom 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)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)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()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() result = await fetch_and_process()
assert result["status"] == "ok"undefined result = await fetch_and_process()
assert result["status"] == "ok"undefinedimport 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)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)| Scope | Lifecycle | Use Case |
|---|---|---|
| Per test (default) | Most fixtures |
| Per test class | Shared setup within class |
| Per module | Expensive setup shared by module |
| Entire test run | Database connections, servers |
@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()| 作用域 | 生命周期 | 适用场景 |
|---|---|---|
| 每个测试用例(默认) | 大多数fixture |
| 每个测试类 | 测试类内的共享初始化 |
| 每个模块 | 模块内共享的耗时初始化操作 |
| 整个测试运行周期 | 数据库连接、服务器等 |
@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()undefinedundefinedundefinedundefined@pytest.mark.asyncio@pytest.mark.asyncio| Anti-Pattern | Why Bad | Fix |
|---|---|---|
| Tests depend on order | Flaky, hard to debug | Use fixtures, isolate |
| Testing implementation | Brittle tests | Test behavior |
| Too many assertions | Hard to identify failure | One assertion per test |
| No error case tests | Missing coverage | Test exceptions explicitly |
| Slow unit tests | Slow feedback | Mock I/O, use in-memory DB |
| 反模式 | 问题所在 | 修复方案 |
|---|---|---|
| 测试依赖执行顺序 | 测试不稳定,难以调试 | 使用fixture,确保测试独立 |
| 测试实现细节 | 测试脆弱,易随代码修改失效 | 测试行为而非实现 |
| 单个测试包含过多断言 | 难以定位失败原因 | 每个测试仅保留一个断言 |
| 未测试错误场景 | 覆盖率不足 | 显式测试异常情况 |
| 单元测试速度慢 | 反馈周期长 | Mock I/O操作,使用内存数据库 |