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":
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:
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:
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 == 1Every 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:
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# tests/conftest.py
import pytest
@pytest.fixture
def sample_user():
return {"name": "Alice", "age": 30}# tests/test_user.py — no imports needed!
def test_adult(sample_user):
assert sample_user["age"] >= 18Note: 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:
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
| Scenario | Recommendation |
|---|---|
| Simple one-liner used by a single test | Write it inline |
| Multiple tests share the same prep/cleanup | Extract into a fixture |
| Need "fresh instance per test" isolation | Fixture + 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
yieldis setup, after is teardown; cleanup runs regardless of test outcome; conftest.pyauto-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
- Write an
empty_cartfixture for aShoppingCartclass (yields an empty cart), plus a dependentcart_with_itemsfixture inserting 3 items; verify each with two tests. - Implement a
log_filefixture 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. - Refactor Chapter 5's
parse_iso_datepractice so valid-date samples come from a conftest-provided fixture.
Comfortable with single fixtures? Next chapter unlocks scopes and sharing in full.