Skip to content

Pytest Fixtures for API Testing — Building Reusable Test Components

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about Pytest Fixtures for API Testing. We cover key concepts, practical examples, and best practices to help you master this topic.

Pytest fixtures provide reusable test components — API clients, authentication tokens, test database sessions, and test data factories — that are injected into test functions automatically.

Code Example: Fixture Organization with conftest.py

# tests/conftest.py — shared fixtures for all API tests
import pytest
import httpx
import jwt
import datetime
from typing import Generator

BASE_URL = "https://api.durga-antivirus.com"

@pytest.fixture(scope="session")
def api_base_url() -> str:
    return BASE_URL

@pytest.fixture(scope="session")
def jwt_secret() -> str:
    return "test-secret-for-testing-only"

@pytest.fixture(scope="session")
def auth_token(jwt_secret: str) -> str:
    """Session-scoped auth token — generated once for all tests."""
    return jwt.encode(
        {
            "sub": "test-analyst",
            "roles": ["analyst"],
            "scope": "threat:read threat:write",
            "exp": datetime.datetime.utcnow() + datetime.timedelta(hours=24)
        },
        jwt_secret,
        algorithm="HS256"
    )

@pytest.fixture
def auth_headers(auth_token: str) -> dict:
    """Function-scoped headers — fresh dict per test."""
    return {"Authorization": f"Bearer {auth_token}"}

@pytest.fixture
def http_client() -> Generator[httpx.Client, None, None]:
    with httpx.Client(base_url=BASE_URL, timeout=30.0) as client:
        yield client

Code Example: Factory Fixtures for Test Data

# tests/factories.py
import pytest
import random

@pytest.fixture
def threat_factory():
    """Factory fixture — creates threat data dicts."""

    def _create_threat(**overrides):
        threat = {
            "name": f"Test Threat {random.randint(1000, 9999)}",
            "severity": random.choice(["low", "medium", "high", "critical"]),
            "source_ip": f"{random.randint(1, 255)}.{random.randint(0, 255)}.{random.randint(0, 255)}.{random.randint(1, 255)}",
            "description": "Created by test factory"
        }
        threat.update(overrides)
        return threat

    return _create_threat

@pytest.fixture
def multiple_threats(threat_factory):
    """Create multiple threat fixtures."""

    def _create_threats(count: int, **shared_overrides):
        return [
            threat_factory(**shared_overrides)
            for _ in range(count)
        ]

    return _create_threats


# Usage in tests
class TestThreatCreation:
    def test_create_valid_threat(self, http_client, auth_headers, threat_factory):
        threat_data = threat_factory(severity="high")
        response = http_client.post(
            "/api/v1/threats",
            json=threat_data,
            headers=auth_headers
        )
        assert response.status_code == 201

    def test_create_multiple_threats(self, http_client, auth_headers, multiple_threats):
        threats = multiple_threats(5, severity="critical")
        for threat in threats:
            response = http_client.post(
                "/api/v1/threats",
                json=threat,
                headers=auth_headers
            )
            assert response.status_code == 201

Code Example: Database Fixtures for Integration Tests

# tests/db_fixtures.py
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from testcontainers.postgres import PostgresContainer

@pytest.fixture(scope="session")
def postgres_container():
    with PostgresContainer("postgres:15") as postgres:
        yield postgres

@pytest.fixture
def db_engine(postgres_container):
    engine = create_engine(postgres_container.get_connection_url())
    Base.metadata.create_all(bind=engine)
    yield engine
    Base.metadata.drop_all(bind=engine)

@pytest.fixture
def db_session(db_engine):
    connection = db_engine.connect()
    transaction = connection.begin()
    session = sessionmaker(bind=connection)()

    yield session

    session.close()
    transaction.rollback()
    connection.close()

@pytest.fixture
def threat_in_db(db_session, threat_factory):
    """Create a threat directly in the database."""
    from api.models import Threat
    data = threat_factory()
    threat = Threat(**data)
    db_session.add(threat)
    db_session.commit()
    db_session.refresh(threat)
    return threat

Common Mistakes

1. Session-Scoped Fixtures Modifying State

Session-scoped fixtures persist across all tests. If a test modifies a session-scoped object, other tests are affected. Use function-scoped fixtures for mutable objects.

2. Fixture Dependency Order Entanglement

Tests should not depend on fixture execution order. Each test receives a clean fixture state. Do not rely on side effects from previous tests.

3. Overusing Session Scope

Session-scoped auth tokens are efficient but difficult to refresh. If the token expires during a long test run, all subsequent tests fail. Balance performance with reliability.

4. Fixtures That Are Too Broad

A fixture that returns everything (client + auth + database + test data) is hard to reuse. Create focused fixtures with single responsibilities.

5. Not Using conftest.py Hierarchically

conftest.py files apply to the directory and subdirectories. Use a root conftest.py for global fixtures and subdirectory conftest.py for module-specific fixtures.

Practice Questions

  1. What are the four fixture scopes in pytest?
  2. How does fixture Dependency Injection work?
  3. What is the purpose of conftest.py?
  4. Why should factory fixtures return a callable rather than data directly?
  5. How do you ensure test isolation with database fixtures?

Answers:

  1. function (default, created per test), class (created per class), module (created per module), session (created once per test run).
  2. pytest matches fixture names to function parameter names. If a test function has a parameter named db_session, pytest finds and executes the db_session fixture.
  3. conftest.py shares fixtures across multiple test files in a directory. Fixtures defined in conftest.py are automatically available to all tests in that directory.
  4. A factory fixture returns a function that creates data. This allows each test to customize the data with overrides while sharing the base creation logic.
  5. Wrap each test in a database Transaction and roll back at the end. Use yield fixtures with session.rollback() in the teardown phase.

Challenge: Build a pytest fixture library for API testing with session-scoped auth token, function-scoped HTTP client, factory fixtures for test data, database fixtures with transaction rollback, and hierarchical conftest.py organization.

FAQ

Can fixtures yield multiple times?

No. A fixture with yield yields once per fixture invocation. Use separate fixtures for setup and teardown if you need multiple phases.

How do I debug fixture execution order?

Use pytest --setup-show to display fixture setup and teardown order. This helps understand when each fixture is created and destroyed.

Can fixtures be async?

Yes. Use @pytest.fixture with async def and await inside the fixture. Use pytest-asyncio to run async fixtures.

How do I skip a fixture for certain tests?

Use @pytest.mark.usefixtures('fixture_name') to apply fixtures to specific tests. Use pytest.mark.skipif with fixture-overriding markers.

What is the difference between conftest.py and a plugin?

conftest.py is project-specific and applies to the current directory. A plugin is installed as a package and applies globally.

Mini Project

Build a pytest fixture library for a threat intelligence API with: session-scoped auth token with auto-refresh, function-scoped httpx client, factory fixtures for threats and investigations, database fixtures with transaction rollback, and hierarchical conftest.py files for modular organization.

What's Next

Now learn about Pytest Mocking for API Tests for mocking external dependencies in API tests.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro