Skip to content

Chapter 6 · Fixture Fundamentals ​

Chapter goals: understand fixtures as dependency injection, declare them, inject by parameter name, implement setup/teardown with yield, and know when they should replace traditional setup functions.

6.1 What a Fixture Really Is ​

A fixture is pytest's dependency-injection (DI) mechanism: write a name into your test's parameter list, and pytest finds the fixture function of that name, executes it, and injects the result. The official docs make the equivalence explicit — a fixture is "a factory function the framework calls and passes in for you":

python
import pytest


class Fruit:
    def __init__(self, name):
        self.name = name
        self.cubed = False

    def cube(self):
        self.cubed = True


@pytest.fixture
def fruit_bowl():
    # Arrange: prepare resources
    return [Fruit("apple"), Fruit("banana")]


def test_fruit_salad(fruit_bowl):
    # just ask for it in the parameter list; pytest runs the fixture and injects it
    salad = [f for f in fruit_bowl if f.cubed]
    assert all(f.name for f in fruit_bowl)

It's essentially equivalent to writing:

python
bowl = fruit_bowl()          # what the framework does for you
test_fruit_salad(fruit_bowl=bowl)

The benefit is immediate: tests declare what they need, not how to build it. One fixture can be shared by dozens of tests; when construction logic changes, you edit one place.

Versus unittest setUp

setUp is inheritance-based — one per class, not reusable elsewhere. Fixtures compose by name: any test or class can reference any number of fixtures on demand, and fixtures can depend on each other.

6.2 yield: Setup and Teardown in One Function ​

Many resources need cleanup after use. yield splits a fixture in two: everything before yield is setup, everything after is teardown:

python
import sqlite3

import pytest


@pytest.fixture
def db_conn():
    # ---- setup ----
    conn = sqlite3.connect(":memory:")
    conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
    yield conn
    # ---- teardown: runs only after the test finishes ----
    conn.close()


def test_insert_user(db_conn):
    db_conn.execute("INSERT INTO users (name) VALUES ('Alice')")
    (count,) = db_conn.execute("SELECT COUNT(*) FROM users").fetchone()
    assert count == 1

Every test using db_conn gets a brand-new in-memory database — solving the first big problem fixtures exist for: state isolation between tests.

Teardown runs regardless of test outcome: even when assertions fail or exceptions raise, code after yield still executes, making cleanup reliable.

6.3 conftest.py: Sharing Fixtures Across Files ​

Fixtures are visible only inside their defining module by default. The standard cross-file location is conftest.py — pytest auto-loads every conftest along the directory tree at collection time, making its fixtures visible to all tests at the same level and below:

text
tests/
├── conftest.py            # project-wide fixtures live here
├── test_user.py           # can use db_conn directly
└── api/
    ├── conftest.py        # fixtures visible only under api/
    └── test_api.py
python
# tests/conftest.py
import pytest


@pytest.fixture
def sample_user():
    return {"name": "Alice", "age": 30}
python
# tests/test_user.py — no imports needed!
def test_adult(sample_user):
    assert sample_user["age"] >= 18

Note: fixtures from conftest require no import — that's pytest resolving names by convention. The trade-off is IDEs may flag "undefined parameter"; installing a pytest plugin fixes recognition.

6.4 Fixtures Depending on Fixtures ​

Fixture parameter lists can request other fixtures too, so pytest resolves the whole dependency tree automatically. This lets you decompose complex setup into compositions of simple steps:

python
import pytest


@pytest.fixture
def user():
    return {"name": "Bob", "orders": []}


@pytest.fixture
def user_with_order(user):
    # depends on another fixture, receiving the same user instance
    user["orders"].append({"id": 1, "amount": 99})
    return user


def test_order_exists(user_with_order):
    assert len(user_with_order["orders"]) == 1


def test_user_name_still_there(user_with_order):
    assert user_with_order["name"] == "Bob"

Don't Over-Chain

Long dependency chains make "who mutated this value?" hard to trace. Keep chains to two or three levels; beyond that consider merging into one fixture.

6.5 Fixture vs Manual Setup: Choosing ​

ScenarioRecommendation
Simple one-liner used by a single testWrite it inline
Multiple tests share the same prep/cleanupExtract into a fixture
Need "fresh instance per test" isolationFixture + yield teardown
Expensive resource shared across all tests (connection pool)Fixture + scope="session" (next chapter)

Rule of thumb: duplicated twice → extract a fixture; any cleanup logic involved → must be a fixture.

6.6 Chapter Summary ​

  • Fixtures are declarative DI: the parameter name is the contract; pytest finds, executes, and injects;
  • Code before yield is setup, after is teardown; cleanup runs regardless of test outcome;
  • conftest.py auto-loads; its fixtures need no import and serve same-level and child directories;
  • Fixtures can depend on other fixtures, but keep chains short;
  • As soon as reuse or isolation needs appear, move from inline preparation to fixtures.

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. In def test_a(cart):, how does cart receive a value?

2. Which statement about yield inside a fixture is correct?

3. How do tests consume fixtures defined in conftest.py?

4. Which scenario most warrants extracting a fixture?

🛠️ Hands-On Exercises ​

  1. Write an empty_cart fixture for a ShoppingCart class (yields an empty cart), plus a dependent cart_with_items fixture inserting 3 items; verify each with two tests.
  2. Implement a log_file fixture using the temp-file pattern: setup creates an empty file, teardown prints and deletes it, and a test writes one line then asserts content exists.
  3. Refactor Chapter 5's parse_iso_date practice so valid-date samples come from a conftest-provided fixture.

Comfortable with single fixtures? Next chapter unlocks scopes and sharing in full.