Skip to content

Framework Directory Structure

Canonical Layout

project-root/
├── tests/
│   ├── e2e/              # Browser-driven end-to-end tests
│   ├── api/              # HTTP/gRPC/WebSocket API tests
│   └── integration/      # Service-to-service integration tests
│
├── framework/
│   ├── core/             # Reusable cross-domain logic
│   │   ├── retry.py
│   │   ├── wait.py
│   │   └── logger.py
│   ├── drivers/          # Communication channel wrappers
│   │   ├── http.py
│   │   ├── browser.py
│   │   └── grpc.py
│   ├── builders/         # Test data builders
│   ├── assertions/       # Custom assertion helpers
│   └── utils/            # Miscellaneous helpers
│
├── data/
│   ├── fixtures/         # Static JSON/YAML test data
│   └── factories/        # Dynamic data factories
│
└── config/
    └── environments/     # Per-environment config files
        ├── dev.yaml
        ├── staging.yaml
        └── prod.yaml

What Goes Where

tests/e2e/

Browser-level user journey tests. Slow. Minimal count. Mirror the application domain, not the file structure of the app.

tests/e2e/
├── auth/
│   ├── test_login.py
│   └── test_logout.py
└── checkout/
    └── test_purchase_flow.py

tests/api/

Fast HTTP/gRPC tests against service endpoints. No browser. No UI. Maximum count.

tests/api/
├── users/
│   ├── test_create_user.py
│   └── test_update_user.py
└── orders/
    ├── test_create_order.py
    └── test_order_status.py

framework/

Not for test cases. Only reusable framework components. If a file in framework/ contains def test_, it is in the wrong place.

data/fixtures/

Static, version-controlled test data. JSON or YAML. Used when data is stable and shared across many tests.

# data/fixtures/products.yaml
- sku: "WIDGET-001"
  name: "Standard Widget"
  price: 9.99

config/environments/

One file per deployment environment. Never commit secrets here — reference env variables.

# config/environments/staging.yaml
base_url: "https://api.staging.example.com"
timeout_seconds: 30
db_host: "${DB_HOST}"

Test Case Structure — Arrange / Act / Assert

Every test follows the same three-phase structure:

def test_user_cannot_order_out_of_stock_item(
    api_client: ApiClient,
    product_factory: ProductFactory,
    user_factory: UserFactory,
) -> None:
    # Arrange
    user = user_factory.create_verified()
    product = product_factory.create(stock=0)

    # Act
    response = api_client.orders.create(
        user_id=user.id,
        items=[product.sku],
    )

    # Assert
    assert response.status_code == 422
    assert response.json()["error"] == "OUT_OF_STOCK"

Rules: - One assertion focus per test (one behaviour, not one assert) - Arrange block creates all state; no state created in Act or Assert - Comments (# Arrange, # Act, # Assert) are required when the block is non-trivial


Naming Conventions

Item Pattern Example
Test file test_<subject>.py test_login.py
Test function test_<scenario> test_invalid_password_returns_401
Page object <Page>Page LoginPage
API client <Domain>Client UsersClient
Builder <Model>Builder UserBuilder
Fixture noun describing what it provides verified_user, empty_cart

conftest.py Placement

Location Contains
tests/conftest.py Global fixtures (driver, config, logging)
tests/api/conftest.py API-specific fixtures (http client, auth)
tests/e2e/conftest.py Browser-specific fixtures (page, browser)

Never put fixtures in conftest that are only used by one test file — put them in that file.