Skip to content

Test Execution Strategies

Parallel Execution

Thread Safety

Running tests in parallel requires every test to be fully isolated.

Requirement Rule
No shared state Fixtures scoped to function, not module/session for mutable data
No shared files Each test writes to a unique path
No shared DB rows Each test creates its own data
No port conflicts Dynamic port allocation for local servers

pytest-xdist setup:

# pytest.ini
[pytest]
addopts = -n auto --dist=loadscope

--dist=loadscope groups tests in the same class/module onto the same worker, preserving class-scoped fixture isolation.

Resource Isolation

For DB-heavy parallel tests, use a separate schema or database per worker:

import pytest
import os

@pytest.fixture(scope="session")
def db_url(worker_id):
    if worker_id == "master":
        return os.environ["TEST_DATABASE_URL"]
    # Each worker gets its own schema
    base_url = os.environ["TEST_DATABASE_URL"]
    return f"{base_url}_{worker_id}"

Selective Execution

Tags / Marks

Group tests with marks for selective running:

import pytest

@pytest.mark.smoke
def test_health_check(api_client):
    assert api_client.get("/health").status_code == 200

@pytest.mark.slow
def test_bulk_import(api_client):
    ...

@pytest.mark.security
def test_sql_injection(api_client):
    ...

Run by mark:

uv run pytest -m smoke           # fast smoke suite
uv run pytest -m "not slow"      # skip slow tests in dev
uv run pytest -m "security"      # security suite only

Test Grouping

Group When to run Marks
Smoke Every commit smoke
Regression Every PR regression
Slow / E2E Nightly or pre-release slow, e2e
Security Nightly security
Performance Release candidate performance

Marks declared in pytest.ini:

[pytest]
markers =
    smoke: Core health checks
    regression: Full regression suite
    slow: Tests taking > 5s
    e2e: End-to-end browser tests
    security: Security validation tests
    performance: Load and latency tests


Retry Strategies

Handling Flaky Tests

Use pytest-rerunfailures for genuinely flaky tests (network, timing):

# pytest.ini
addopts = --reruns 2 --reruns-delay 1

Or per-test:

@pytest.mark.flaky(reruns=3, reruns_delay=2)
def test_external_webhook_delivery(api_client):
    ...

Rules: - Retry only on known transient failures, not logic errors - Track retry rate — high retry rate signals a design problem - Never use retry as a substitute for fixing flakiness

Retry with Backoff (custom)

import time
import logging

logger = logging.getLogger(__name__)


def retry(fn, retries: int = 3, delay: float = 1.0, backoff: float = 2.0):
    last_error = None
    for attempt in range(1, retries + 1):
        try:
            return fn()
        except AssertionError as exc:
            last_error = exc
            logger.warning("Attempt %d/%d failed: %s", attempt, retries, exc)
            if attempt < retries:
                time.sleep(delay)
                delay *= backoff
    raise last_error

Execution Speed Optimisation

Technique Impact
Parallel execution (-n auto) 2–8x faster
Session-scoped DB fixtures Avoid repeated migrations
Skip DB for unit tests Sub-millisecond per test
Reuse HTTP session Remove per-test connection overhead
Selective marks in CI Only run relevant suite per stage