Use when writing or running Python tests with pytest. Covers fixtures, mocking, parametrization, freezegun for time, and async testing. Triggers on test file creation, fixture design, mocking external dependencies, or test suite architecture. Extends python-development skill.
/plugin marketplace add btimothy-har/mycc-config/plugin install brian-claude-skills@brian-claudeThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Extends the python-development skill. Use pytest for all testing.
Test Design
test_create_user_raises_on_duplicate_email not test_create_user_2Fixtures
conftest.py for shared, inline for test-specificsession for expensive setup (DB engine), function for test isolation (sessions, data)tmp_path, monkeypatch, capsys, caplog before custom solutionsMocking
myapp.service.requests.get, not requests.getassert_called_once_with when the call itself is the behavior being testedside_effect for sequences - Return different values on successive calls or raise exceptionsOrganization
tests/test_users.py tests myapp/users.py@pytest.mark.parametrize instead of copy-pasting testsuv run pytest # Run all tests
uv run pytest tests/test_api.py # Single file
uv run pytest -k "test_create" # Match test names
uv run pytest -x # Stop on first failure
uv run pytest --tb=short # Shorter tracebacks
uv run pytest -v --tb=long # Verbose with full tracebacks
# pyproject.toml
[project.optional-dependencies]
dev = [
"pytest",
"pytest-asyncio",
"pytest-mock",
"freezegun",
"httpx", # For FastAPI TestClient
]
# tests/test_users.py
import pytest
from myapp.users import create_user, get_user
class TestCreateUser:
def test_creates_user_with_valid_email(self, db_session):
user = create_user(db_session, email="test@example.com", name="Test")
assert user.id is not None
assert user.email == "test@example.com"
def test_raises_on_duplicate_email(self, db_session, existing_user):
with pytest.raises(ValueError, match="already exists"):
create_user(db_session, email=existing_user.email, name="Other")
# tests/conftest.py
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from myapp.database import Base
@pytest.fixture(scope="session")
def engine():
"""Create test database engine once per session."""
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
yield engine
engine.dispose()
@pytest.fixture
def db_session(engine):
"""Fresh database session for each test, rolled back after."""
connection = engine.connect()
transaction = connection.begin()
Session = sessionmaker(bind=connection)
session = Session()
yield session
session.close()
transaction.rollback()
connection.close()
@pytest.fixture
def existing_user(db_session):
"""Pre-created user for tests that need one."""
from myapp.models import User
user = User(email="existing@example.com", name="Existing")
db_session.add(user)
db_session.commit()
return user
@pytest.fixture
def make_user(db_session):
"""Factory to create users with custom attributes."""
created = []
def _make_user(email="test@example.com", name="Test", **kwargs):
from myapp.models import User
user = User(email=email, name=name, **kwargs)
db_session.add(user)
db_session.commit()
created.append(user)
return user
yield _make_user
# Cleanup if needed
for user in created:
db_session.delete(user)
db_session.commit()
from unittest.mock import patch, MagicMock
class TestPaymentProcessor:
def test_processes_payment_successfully(self):
# Patch where it's USED, not where it's defined
with patch("myapp.payments.stripe.Charge.create") as mock_charge:
mock_charge.return_value = MagicMock(id="ch_123", status="succeeded")
result = process_payment(amount=1000, token="tok_visa")
assert result.charge_id == "ch_123"
mock_charge.assert_called_once_with(amount=1000, source="tok_visa")
def test_handles_payment_failure(self):
with patch("myapp.payments.stripe.Charge.create") as mock_charge:
mock_charge.side_effect = stripe.error.CardError("declined", None, None)
with pytest.raises(PaymentError, match="declined"):
process_payment(amount=1000, token="tok_bad")
def test_sends_welcome_email(mocker):
mock_send = mocker.patch("myapp.users.send_email")
create_user(email="new@example.com", name="New")
mock_send.assert_called_once_with(
to="new@example.com",
template="welcome",
)
def test_fetches_external_data(mocker):
mock_response = mocker.Mock()
mock_response.json.return_value = {"data": "value"}
mock_response.raise_for_status = mocker.Mock()
mocker.patch("httpx.get", return_value=mock_response)
result = fetch_external_data("https://api.example.com")
assert result == {"data": "value"}
from freezegun import freeze_time
from datetime import datetime
class TestSubscription:
@freeze_time("2024-01-15 10:00:00")
def test_subscription_active_before_expiry(self):
sub = Subscription(expires_at=datetime(2024, 1, 20))
assert sub.is_active() is True
@freeze_time("2024-01-25 10:00:00")
def test_subscription_inactive_after_expiry(self):
sub = Subscription(expires_at=datetime(2024, 1, 20))
assert sub.is_active() is False
def test_trial_duration(self):
with freeze_time("2024-01-01") as frozen:
trial = start_trial()
assert trial.days_remaining == 14
frozen.tick(delta=timedelta(days=7))
assert trial.days_remaining == 7
@pytest.mark.parametrize("email,valid", [
("user@example.com", True),
("user@sub.example.com", True),
("invalid", False),
("@example.com", False),
("user@", False),
("", False),
])
def test_email_validation(email, valid):
assert is_valid_email(email) == valid
@pytest.mark.parametrize("a,b,expected", [
(1, 2, 3),
(0, 0, 0),
(-1, 1, 0),
(100, 200, 300),
])
def test_addition(a, b, expected):
assert add(a, b) == expected
import pytest
# Mark entire module as async
pytestmark = pytest.mark.asyncio
async def test_async_fetch(db_session):
result = await fetch_user_async(db_session, user_id=1)
assert result.name == "Test"
# Or mark individual tests
class TestAsyncOperations:
@pytest.mark.asyncio
async def test_concurrent_requests(self):
results = await asyncio.gather(
fetch_data("endpoint1"),
fetch_data("endpoint2"),
)
assert len(results) == 2
import pytest
from httpx import AsyncClient, ASGITransport
from myapp.main import app
@pytest.fixture
async def client():
async with AsyncClient(
transport=ASGITransport(app=app),
base_url="http://test"
) as client:
yield client
@pytest.mark.asyncio
async def test_create_user_endpoint(client):
response = await client.post("/users", json={"email": "a@b.com", "name": "A"})
assert response.status_code == 201
assert response.json()["email"] == "a@b.com"
@pytest.mark.asyncio
async def test_get_user_not_found(client):
response = await client.get("/users/99999")
assert response.status_code == 404
tests/
├── conftest.py # Shared fixtures
├── test_models.py # Unit tests for models
├── test_services.py # Unit tests for business logic
├── test_api.py # API endpoint tests
└── integration/
├── conftest.py # Integration-specific fixtures
└── test_workflows.py