Chapter 7 · Fixtures Advanced: Scopes and conftest
Chapter goals: master the four
scopelevels — instantiation timing and shared semantics, understand conftest.py's hierarchical override mechanism plusautouseinjection, and learn to trade off "shared savings" against "state isolation" correctly.
7.1 scope: How Long a Fixture Lives
By default every test receives a newly created fixture instance (scope="function"). When construction is expensive (a real SMTP connection, a database engine, a Docker container), raise the scope so the instance gets reused:
# content of conftest.py
import smtplib
import pytest
@pytest.fixture(scope="module")
def smtp_connection():
# created once per test module; all tests in the module share it
return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)# content of test_module.py — both tests receive the same object
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
def test_noop(smtp_connection):
resp = smtp_connection.noop()
assert resp[0] == 250The five official values and their destruction timing:
| scope | Instantiated | Teardown runs |
|---|---|---|
function (default) | Before each test | After that test ends |
class | Before first test of each class | After the class's last test |
module | Before first test of each module | After the module's last test |
package | Before first test in the package | After package's last test |
session | On first request during the session | After all tests finish |
High Scope + Mutable State = Disaster
Once you promote scope to module/session, every test shares one mutable state. One test appends to a shared list or mutates a shared dict, and later tests fail mysteriously. Principle: high-scope fixtures should provide read-only or immutable resources (connections, config objects); anything whose state changes stays function-scoped.
7.2 conftest Hierarchy: Closest Wins
conftest.py files apply hierarchically: each conftest is visible only within its directory and below. When two layers define same-named fixtures, the layer closer to the test wins — enabling "defaults + local overrides":
tests/
├── conftest.py # def base_url(): return "https://api.prod.example.com"
└── local/
├── conftest.py # def base_url(): return "http://localhost:8000"
└── test_local.py # base_url resolves to the localhost version here# tests/conftest.py
import pytest
@pytest.fixture
def base_url() -> str:
"""Project-wide default points at staging"""
return "https://staging.example.com"# tests/local/conftest.py
import pytest
@pytest.fixture
def base_url() -> str:
"""Tests under local/ all hit the local service — same-name override"""
return "http://localhost:8000"This also applies to customizing third-party plugins' fixtures — key to pytest's "convention over configuration" design.
7.3 autouse: Fixtures You Never Ask For
Some cross-cutting concerns (temp cwd switching, network blocking, injecting common patches) are needed by every test; listing them everywhere is tedious. autouse=True makes every test request the fixture automatically:
# tests/conftest.py
import os
import pytest
@pytest.fixture(autouse=True)
def _isolated_cwd(tmp_path):
"""Run every test inside a temp dir so the repo stays clean"""
old = os.getcwd()
os.chdir(tmp_path) # setup: enter temp dir
yield tmp_path
os.chdir(old) # teardown: restore original cwd
@pytest.fixture(autouse=True)
def _fail_on_slow(monkeypatch):
# autouse can be side-effect only (return nothing)
monkeypatch.setenv("APP_ENV", "testing")# any test file — enjoys both environments without declaring anything
def test_writes_config():
with open("config.yaml", "w") as f:
f.write("debug: true")
assert os.path.exists("config.yaml") # written into tmp_path, repo untouchedautouse respects scope too; it may still return values — "available without being requested". Use with restraint: autouse is implicit global behavior; once numerous, nobody can describe a given test's full execution environment anymore.
7.4 Scope Conflicts: High Cannot Depend on Low
One hard rule: higher-scope fixtures cannot depend on lower-scope ones. A session fixture depending on a function fixture raises ScopeMismatch — a session instance is built once while its dependency would need rebuilding per test, which cannot logically hold.
Common pitfall and fix:
@pytest.fixture(scope="session")
def engine():
return create_engine("sqlite:///:memory:")
@pytest.fixture(scope="session")
def tables(engine): # session depends on session ✔
...If session-level initialization truly needs per-test data, invert the structure: make the "expensive but immutable" part high-scope, and place per-test differences in lower-scope fixtures referencing it.
7.5 Decision Table
| Need | Choose |
|---|---|
| Cheap construction, mutable state (lists/model objects) | function (default) |
| Expensive connections, process pools, containers used read-only | module / session |
| All tests under some directory need environment/config swap | Override same-name fixture in that directory's conftest.py |
| Global cross-cutting behavior (cwd switch, env vars, net block) | conftest + autouse=True |
7.6 Chapter Summary
- Five scope levels: function/class/module/package/session determine instance creation and teardown timing;
- High scopes save time but share state — only read-only resources should be promoted;
- Layered conftest + closest-name-wins overrides = the standard pattern for project defaults with local customization;
autouse=Trueprovides declaration-free cross-cutting setup/teardown — use sparingly;- session/module fixtures must not depend on lower scopes (
ScopeMismatch).
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. When does a scope="module" fixture run its teardown?
2. A session-scoped fixture depends on a function-scoped one. What happens?
3. tests/api/conftest.py and tests/conftest.py both define api_client. Which version do tests under tests/api/ get?
4. Which use case fits autouse best?
🛠️ Hands-On Exercises
- Change Chapter 6's
db_connfixture to module then session scope; insert data in one test and write a second verifying it can see the first test's data — feel the double-edged sword of shared state — then restorefunctionand explain why. - Define
app_mode = "prod"in the project-root conftest.py, override it to"test"in some subdirectory's conftest.py, and write a test printing the value to confirm nearest-override works. - Write an autouse fixture recording start/end timestamps per test and printing elapsed time in teardown; run your whole suite and watch the output.
The core mental model of fixtures is now complete. Next chapter advances into factory patterns, finalizers, and the
requestobject.