Skip to content

Test Anti-Patterns and Risks

Test Anti-Patterns

1. Flaky Tests

Description: Tests that pass or fail non-deterministically.

Causes: time.sleep(), shared state, async timing, external service dependency.

Symptoms: - Tests pass locally, fail in CI - Tests fail on re-run without code change - Tests pass in isolation, fail in suite

Fix: - Replace sleep with wait_until polling - Isolate state with function-scoped fixtures - Mock unstable external dependencies


2. Hardcoded Data

Description: Literal IDs, emails, or values embedded in test bodies.

# Bad
def test_get_user():
    response = client.get("/users/42")  # hardcoded ID
    assert response.json()["email"] == "john@example.com"  # hardcoded value

Fix:

def test_get_user(created_user, api_client):
    response = api_client.get(f"/users/{created_user['id']}")
    assert response.json()["email"] == created_user["email"]


3. Over-Mocking

Description: Every dependency mocked, including ones that should be real.

Symptom: Tests pass, but the real system is broken because wiring was never tested.

Fix: - Mock at the boundary (HTTP, DB), not inside business logic - Complement with integration tests that use real dependencies - Use contract tests to verify mocks stay in sync with real APIs


4. UI-Heavy Tests

Description: Business logic and data validation tested through the browser instead of the API.

Symptom Impact
80 %+ tests are E2E Slow feedback (minutes per run)
DB state asserted via UI Fragile, breaks on visual changes
All tests launch a browser High resource consumption

Fix: Move business logic tests to API layer. Use E2E only for critical user journeys.


5. Tight Coupling to Implementation

Description: Tests assert on internal method calls, object attributes, or SQL queries.

# Bad — testing internals
def test_user_service_calls_repo():
    mock_repo = MagicMock()
    service = UserService(repo=mock_repo)
    service.create_user(email="a@b.com")
    mock_repo._session.add.assert_called_once()  # internal detail

Fix: Test behaviour through the public interface. Assert on the output, not the mechanism.


6. Test Duplication (God Test)

Description: One test function asserts on 15 different things.

Fix: Split into focused tests. One logical scenario per test.


Risks and Limitations

Maintenance Cost

Risk Description Mitigation
Tests break on every refactor Coupled to implementation Test through public interfaces
Fixture explosion 50+ fixtures across project Group by domain, document each
Builder drift Builder returns stale schema Review builders on schema migration PRs

Slow Execution

Risk Description Mitigation
Full suite takes 30+ min Too many E2E / integration tests Pyramid balance, parallel execution
DB setup overhead Migration runs per test Session-scoped DB, transaction rollback
Playwright slow Browser tests block CI Headless, parallel workers, skip in unit stage

False Positives / Negatives

Type Example Mitigation
False positive Test passes due to over-mocking Contract + integration tests
False negative Test fails due to environment issue Retry + quarantine + stable env

Environment Dependency

Risk Mitigation
Test relies on staging data Use isolated test accounts
Test uses production keys Always use test-only credentials
Test fails on timezone difference Use UTC everywhere; freeze time in tests

Risk Register Summary

Risk Severity Probability Mitigation
Flaky tests block CI High High Wait strategies, isolation, quarantine
Mocks drift from real API High Medium Contract tests, Pact Broker
E2E suite too slow Medium High Pyramid balance, parallel execution
Tests tied to UI selectors Medium High data-testid attributes
Missing test coverage High Medium Coverage gate in CI (100 %)