Test Architecture
Test Layers
UI Layer
Responsibilities: - Browser or native app interactions - User journey validation - Accessibility and visual regression
Tools: Playwright, Selenium
Rules:
- Thin layer — delegate to Page Objects or Screenplay actors
- No business logic inside UI test steps
- Stable locators: data-testid or ARIA roles, never CSS class names
API Layer
Responsibilities: - HTTP / gRPC / WebSocket protocol validation - Schema and contract verification - Auth flows, error handling
Tools: httpx, pytest, grpcio, playwright (network interception)
Rules: - Request construction separate from assertion - Schema validation on every response - Auth tokens managed by fixture, not hardcoded
Service Layer
Responsibilities: - Internal business logic through service interfaces - Database state validation - Message queue interactions
Tools: direct service class instantiation, in-memory fakes, testcontainers
Rules: - No HTTP in service-layer tests - Side effects verified through output state, not internal calls
Test Structure
Test Suites
A suite groups related scenarios sharing the same:
- Subject under test (e.g. UserService)
- Context (e.g. unauthenticated requests)
- Protocol (e.g. WebSocket connection lifecycle)
Naming: <Subject>_<Context>_Tests
Test Modules
| Module type | Contents |
|---|---|
| Happy path | Core success scenarios |
| Error handling | 4xx / 5xx / domain errors |
| Edge cases | Boundary values, empty inputs |
| Security | Auth bypass, injection attempts |
| Performance | Latency SLO checks |
One module = one .py file, one class or set of related functions.
Test Cases
| Property | Rule |
|---|---|
| Name | Describes scenario and expected result |
| Length | ≤ 30 lines (longer = extract helpers) |
| Assertions | One logical assertion group per test |
| Dependencies | Injected via fixture, never imported globally |
Separation of Concerns
Test Logic vs Test Data
| Concern | Location |
|---|---|
| What is being verified | Test function body |
| How inputs are built | Builder / Factory / Fixture |
| Where data comes from | Fixture file or factory method |
Never embed raw data structures inside test assertions. Always name and extract them.
Assertions vs Setup
Bad pattern — mixed concerns:
def test_create_user():
db.execute("INSERT INTO users ...") # setup mixed with test
response = client.post("/users", json={...})
db_row = db.execute("SELECT ...") # teardown inside test
assert response.status_code == 201
Good pattern:
@pytest.fixture
def existing_user(db_session):
user = UserFactory.create()
yield user
db_session.rollback()
def test_create_user(api_client, existing_user):
response = api_client.post("/users", json=UserBuilder().email("a@b.com").build())
assert response.status_code == 201
Architecture Decision Table
| Need | Pattern |
|---|---|
| Reuse setup across suites | Fixture |
| Build complex test objects | Builder |
| Abstract UI interactions | Page Object / Screenplay |
| Validate HTTP contracts | Schema validator + API client wrapper |
| Isolate external services | Fake / Stub / Mock |
| Run in parallel safely | Scoped fixtures + isolated data |