mirror of
https://github.com/duthaho/claudekit.git
synced 2026-06-26 03:14:38 +03:00
6.7 KiB
6.7 KiB
pytest Fixture Patterns
Catalog of reusable fixture patterns for common testing scenarios.
1. Factory Fixture
Create multiple instances with customizable defaults.
import pytest
from dataclasses import dataclass
@pytest.fixture
def make_user():
"""Factory fixture: creates User instances with sensible defaults."""
created = []
def _make_user(
name: str = "Test User",
email: str | None = None,
is_active: bool = True,
):
if email is None:
email = f"user-{len(created)}@test.com"
user = User(name=name, email=email, is_active=is_active)
created.append(user)
return user
yield _make_user
# Cleanup: delete all created users
for user in created:
user.delete()
Usage:
def test_deactivate_user(make_user):
user = make_user(name="Alice", is_active=True)
user.deactivate()
assert not user.is_active
2. Database Session (SQLAlchemy)
Transaction-isolated database session that rolls back after each test.
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
@pytest.fixture(scope="session")
def engine():
"""Create a test database engine (once per test session)."""
engine = create_engine("postgresql://test:test@localhost:5432/test_db")
yield engine
engine.dispose()
@pytest.fixture(scope="session")
def tables(engine):
"""Create all tables once, drop after all tests."""
Base.metadata.create_all(engine)
yield
Base.metadata.drop_all(engine)
@pytest.fixture
def db_session(engine, tables):
"""Provide a transactional database session that rolls back after each test."""
connection = engine.connect()
transaction = connection.begin()
session = sessionmaker(bind=connection)()
yield session
session.close()
transaction.rollback()
connection.close()
3. Temporary Files and Directories
@pytest.fixture
def sample_config(tmp_path: Path) -> Path:
"""Create a temporary config file with test content."""
config = tmp_path / "config.yaml"
config.write_text(
"""\
database:
host: localhost
port: 5432
debug: true
"""
)
return config
@pytest.fixture
def data_dir(tmp_path: Path) -> Path:
"""Create a temporary directory structure for testing."""
(tmp_path / "input").mkdir()
(tmp_path / "output").mkdir()
(tmp_path / "input" / "data.csv").write_text("id,name\n1,Alice\n2,Bob\n")
return tmp_path
4. Mock External Service
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
@pytest.fixture
def mock_http_client():
"""Mock an HTTP client with pre-configured responses."""
client = MagicMock()
client.get.return_value = MagicMock(
status_code=200,
json=lambda: {"status": "ok"},
)
client.post.return_value = MagicMock(
status_code=201,
json=lambda: {"id": "new-123"},
)
return client
@pytest.fixture
def mock_payment_gateway():
"""Mock a payment gateway service."""
with patch("app.services.payment.PaymentGateway") as mock_cls:
instance = mock_cls.return_value
instance.charge.return_value = {
"transaction_id": "txn-test-123",
"status": "succeeded",
}
instance.refund.return_value = {
"refund_id": "ref-test-456",
"status": "refunded",
}
yield instance
# Async version
@pytest.fixture
def mock_email_service():
"""Mock an async email service."""
with patch("app.services.email.EmailService") as mock_cls:
instance = mock_cls.return_value
instance.send = AsyncMock(return_value={"message_id": "msg-test-789"})
yield instance
5. Authenticated Test Client (FastAPI)
import pytest
from httpx import AsyncClient, ASGITransport
from app.main import app
from app.auth import create_access_token
@pytest.fixture
def auth_token():
"""Generate a valid JWT token for testing."""
return create_access_token(
data={"sub": "test-user-id", "role": "admin"},
expires_minutes=60,
)
@pytest.fixture
def auth_headers(auth_token: str) -> dict[str, str]:
"""HTTP headers with Bearer token."""
return {"Authorization": f"Bearer {auth_token}"}
@pytest.fixture
async def client():
"""Unauthenticated async test client."""
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as c:
yield c
@pytest.fixture
async def auth_client(auth_headers):
"""Authenticated async test client."""
transport = ASGITransport(app=app)
async with AsyncClient(
transport=transport,
base_url="http://test",
headers=auth_headers,
) as c:
yield c
6. Environment Variables
@pytest.fixture
def env_vars(monkeypatch):
"""Set environment variables for the test, automatically restored after."""
monkeypatch.setenv("DATABASE_URL", "postgresql://test:test@localhost/test")
monkeypatch.setenv("SECRET_KEY", "test-secret-key")
monkeypatch.delenv("PRODUCTION_API_KEY", raising=False)
7. Freezing Time
@pytest.fixture
def frozen_time():
"""Freeze time to a specific moment."""
fixed = datetime(2025, 1, 15, 12, 0, 0, tzinfo=timezone.utc)
with patch("app.services.datetime") as mock_dt:
mock_dt.now.return_value = fixed
mock_dt.utcnow.return_value = fixed
mock_dt.side_effect = lambda *a, **kw: datetime(*a, **kw)
yield fixed
Alternative: use freezegun library with @freeze_time("2025-01-15 12:00:00").
8. Parametrized Fixture
@pytest.fixture(params=["sqlite", "postgresql"])
def db_url(request):
"""Run tests against multiple database backends."""
urls = {
"sqlite": "sqlite:///test.db",
"postgresql": "postgresql://test:test@localhost/test",
}
return urls[request.param]
Fixture Scope Reference
| Scope | Lifetime | Use For |
|---|---|---|
function (default) |
Each test | Most fixtures, mutable state |
class |
Each test class | Shared setup for a class |
module |
Each test file | Expensive setup shared across file |
session |
Entire test run | Database engine, heavy resources |
Tips
- Use
yield(notreturn) when cleanup is needed after the test. - Use
autouse=Truesparingly -- only for things every test needs. - Keep fixtures small and composable -- combine them in tests, not in other fixtures.
- Use
monkeypatchinstead ofunittest.mock.patchfor env vars and attributes when possible. - Name fixtures after what they provide, not what they do:
db_sessionnotsetup_database.