Testing Patterns Expertise
Load this skill when writing tests, designing test architecture, or debugging test failures.
Python Testing (pytest)
Fixture Patterns
- Factory fixtures: Use
@pytest.fixturereturning a factory function for flexible test data creation - Scoped fixtures:
sessionfor expensive setup (DB connections),modulefor shared state,function(default) for isolation - Fixture composition: Small focused fixtures composed together, not monolithic setup fixtures
- conftest.py hierarchy: Project-level shared fixtures in root conftest, module-specific in local conftest
tmp_path/tmp_path_factory: Always use pytest's built-in temp directories over manual tempfileautousesparingly: Only for truly universal setup (e.g., reset global state); explicit is better
Mocking Strategy
- Prefer
monkeypatchoverunittest.mockfor attribute/env patching - it auto-reverts - Mock at the boundary: Mock external services and I/O, not internal logic
monkeypatch.setattr: For replacing functions/methods on objectsmonkeypatch.setenv/monkeypatch.delenv: For environment variablesunittest.mock.AsyncMock: For async functions;monkeypatchdoesn't handle async nativelyrespxfor httpx mocking,responsesfor requests mocking - prefer these over generic mocks for HTTP- Never mock what you don't own without an adapter layer
freezegunortime-machinefor time-dependent tests
Parametrize Patterns
# Prefer parametrize over copy-pasted test functions
@pytest.mark.parametrize("input_val,expected", [
("valid@email.com", True),
("no-at-sign", False),
("", False),
])
def test_email_validation(input_val: str, expected: bool) -> None:
assert validate_email(input_val) == expected
# Use ids for readable test names
@pytest.mark.parametrize("status_code", [400, 401, 403, 404, 500], ids=str)
Async Testing
pytest-asynciowithmode = "auto"in pyproject.toml to avoid decorating every testanyiofor backend-agnostic async tests (supports both asyncio and trio)httpx.AsyncClientwith FastAPI'sASGITransportfor async API testing - no need forTestClient- Async fixtures: Mark with
@pytest.fixtureand useasync def- pytest-asyncio handles it
Property-Based Testing (Hypothesis)
- Use for: Input validation, serialization roundtrips, mathematical properties, parsers
@given(st.text()): Test with arbitrary strings instead of hand-picked examples@example("edge_case"): Pin known edge cases alongside generated onesst.builds(MyModel): Auto-generate Pydantic/dataclass instances from their type hints- Settings:
@settings(max_examples=200)in CI, lower locally for speed - Stateful testing:
RuleBasedStateMachinefor testing stateful APIs
Test Architecture
- Arrange-Act-Assert: One logical assertion per test, clear separation of phases
- Test naming:
test_<unit>_<scenario>_<expected>(e.g.,test_login_invalid_password_returns_401) - Directory structure: Mirror source layout -
src/auth/service.py->tests/auth/test_service.py - Markers: Use
@pytest.mark.slow,@pytest.mark.integration,@pytest.mark.unitand select with-m - Coverage:
pytest-covwith--cov-fail-under=90, exclude protocol classes and abstract methods
Database Testing
- Use transactions: Wrap each test in a transaction and rollback - faster than recreating tables
pytest-postgresqlor testcontainers for real DB tests- Factory Boy:
factory.Factorywith SQLAlchemy integration for model factories - Separate unit from integration: Unit tests mock the DB layer, integration tests use real DB
FastAPI Testing
# Prefer async client for async endpoints
from httpx import ASGITransport, AsyncClient
@pytest.fixture
async def client(app: FastAPI) -> AsyncGenerator[AsyncClient, None]:
async with AsyncClient(
transport=ASGITransport(app=app),
base_url="http://test",
) as ac:
yield ac
async def test_create_user(client: AsyncClient) -> None:
response = await client.post("/users", json={"name": "test"})
assert response.status_code == 201
Go Testing
Table-Driven Tests
func TestParseURL(t *testing.T) {
tests := []struct {
name string
input string
want *URL
wantErr bool
}{
{"valid http", "http://example.com", &URL{Scheme: "http"}, false},
{"empty string", "", nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseURL(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("ParseURL() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Use go-cmp for struct comparison
if diff := cmp.Diff(tt.want, got); diff != "" {
t.Errorf("ParseURL() mismatch (-want +got):\n%s", diff)
}
})
}
}
Go Testing Patterns
t.Helper(): Call in test helper functions so failures report the caller's linet.Parallel(): Add to independent tests for faster executiont.Cleanup(func()): Register cleanup instead of defer for better test lifecycle managementtestify/assertvs stdlib: Stdlib for simple projects, testify for complex assertionsgo-cmp: Prefer overreflect.DeepEqualfor struct comparison - better diffshttptest.NewServer: For testing HTTP clients against a real servert.TempDir(): Auto-cleaned temp directory per test- Golden files: Store expected output in
testdata/and compare withos.ReadFile+cmp.Diff - Build tags:
//go:build integrationto separate unit from integration tests
When to Use This Skill
- Writing new tests or improving test coverage
- Designing test architecture for a new project
- Debugging flaky or slow tests
- Setting up test fixtures, factories, or mocking strategies
- Choosing between testing approaches (unit vs integration, mock vs real)
