January 20, 2026

#4 - Pytest Concept

api-testingpythonpytest

pytest is one of the most widely adopted testing frameworks in the Python ecosystem. It provides a clean, expressive, and highly extensible way to write tests — ranging from simple unit tests to large-scale API and integration test suites.

Tests are written as regular functions and classes, assertions use native Python syntax, and structure is driven by conventions rather than rigid frameworks. This makes tests easy to read, easy to maintain, and straightforward to scale as projects grow.

conftest

conftest.py is a pytest configuration and dependency injection file. It's used to:

  • Define shared fixtures
  • Configure test-wide behavior
  • Expose fixtures without explicit imports
  • Control fixture visibility by directory scope

Pytest automatically discovers conftest.py files by walking up the directory tree.

Fixtures defined in tests/conftest.py are:

  • Available to all tests under tests/
  • Imported implicitly
  • Resolved via fixture dependency injection

conftest is used for

  1. Shared fixtures
  2. Environment and configuration setup
    • Base URLs
    • Environment selection (--env=dev)
    • Auth tokens
    • Sessions
  3. API client/sessions
  4. Pytest hooks and options
    • pytest_runtest_setup
    • pytest_sessionstart

conftest should not be used for

  1. Test logic
  2. Assertions
  3. One-off fixtures used by a single test
  4. Business logic

Rules

  • Fixtures apply downward only
  • The closest conftest.py is used
  • Enables bounded context per domain
tests/
├── conftest.py          # global fixtures
├── users/
│   ├── conftest.py      # user-specific fixtures
│   └── test_users.py

Fixture

A fixture is a dependency provider — it handles setup before a test runs, reuse of that setup across many tests, and teardown after the test.

Key idea:

  • A test does not create the user
  • A test declares what it needs
  • pytest injects the dependency
  • Fixtures can depend on other fixtures

Fixture scope

  • function (default) — per test function
  • class — per test class
  • module — per test file
  • package — per folder
  • session — entire test run

Setup and teardown using a yield fixture

@pytest.fixture
def temp_user(user_client):
    user = user_client.create_user()
    yield user
    user_client.delete_user(user["id"])

Execution order:

  1. Code before yield → setup
  2. Test runs
  3. Code after yield → teardown (even if the test fails)

Fixture parametrization

Fixtures can produce multiple variants — the same test runs multiple times, once per fixture value, keeping test logic cleanly separated from test data.

@pytest.fixture(params=["admin", "user"])
def role(request):
    return request.param
def test_access(role):
    ...

Fixture override and conftest

conftest.py is the central place for shared fixtures — no imports needed, scoped by directory tree.

Overriding fixtures

  • A lower-level conftest.py overrides higher ones
  • Useful for environment-specific behavior
  • Useful for test-specific customization

Pytest execution

At a high level, pytest follows a simple flow:

  1. Find tests
    • Pytest looks for test files, functions, and classes that follow its naming rules
    • Files starting with test_ or ending with _test.py
    • Functions starting with test_
    • Classes starting with Test
  2. Prepare test requirements
    • If tests use fixtures, pytest figures out what each test needs and builds a dependency plan. Fixtures are set up in the correct order before the test runs.
  3. Run the tests
    • Pytest executes the tests one by one. During execution: fixtures are created, test code runs, assertions are evaluated, and if something goes wrong, the test is marked as failed or errored.
  4. Report the results
    • After execution, pytest shows a summary: passed tests, failed tests, and skipped tests. For failures, pytest also shows error messages and stack traces to help with debugging.

Originally published on Hashnode.