Fixture scope and conftest.py — sharing setup safely
Build slow setup once and share it: the five scopes, --setup-show, the danger of shared state and ScopeMismatch, conftest.py for fixtures without imports and per-directory overrides, what autouse costs, and the order fixtures are created in.
- 1Encounter
- 2Understand
- 3Worked
- 4Predict
- 5Apply
- 6Stretch
The problem we are solving
Last chapter's fixtures were built fresh for every test. That is the right default — until the thing being built is expensive. Here is a database connection that takes half a second to open, the way a real server handshake might:
test_users.py:
import sqlite3
import time
import pytest
def connect():
time.sleep(0.5) # stands in for a slow server handshake
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE users (name TEXT)")
return conn
@pytest.fixture
def db():
conn = connect()
yield conn
conn.close()
def test_starts_empty(db):
count = db.execute("SELECT COUNT(*) FROM users").fetchone()[0]
assert count == 0
def test_insert_one(db):
db.execute("INSERT INTO users VALUES ('asha')")
count = db.execute("SELECT COUNT(*) FROM users").fetchone()[0]
assert count == 1
def test_names_are_text(db):
db.execute("INSERT INTO users VALUES ('ravi')")
name = db.execute("SELECT name FROM users").fetchone()[0]
assert name == "ravi"pytest -q --durations=3 reports where the time went:
... [100%]
============================= slowest 3 durations ==============================
0.50s setup test_users.py::test_starts_empty
0.50s setup test_users.py::test_insert_one
0.50s setup test_users.py::test_names_are_text
3 passed in 1.65sThree tests, three connections, a second and a half spent shaking hands. With three hundred tests that is two and a half minutes of nothing, and a suite that slow stops being run.
The question this chapter answers is: how do you build something once and share it — without letting the tests that share it trip over each other?
By the end of this chapter you can
- Choose a fixture's scope:
function,class,module,packageorsession - Watch fixtures being set up and torn down with
--setup-show - Explain why a wide-scoped fixture holding mutable state makes tests depend on each other — and fix it
- Read a
ScopeMismatcherror and know which fixture to change - Share fixtures through
conftest.py, and override one for a single directory - Say what
autouse=Truecosts, and list every fixture available with--fixtures - Predict the order in which fixtures are created
Prerequisites: fixtures.
Before you write the test
Scope is not something you add at the end to make a slow suite fast. It is decided before the first test is written, from two questions about every piece of setup: how much does it cost to build, and does a test change it? Answer those first, and the right scope follows.
The contract. The code under test here is a small users table. What it promises: a new database has no users; inserting a user adds exactly one row; the name you insert is the name you get back. Each of those promises must hold no matter which tests ran before. That last clause is the one this chapter is really about.
What you must have in place. A virtual environment with pytest installed; sqlite3, which ships with Python, so there is nothing to install; the code under test importable from the test files (here, files side by side in one directory, run from that directory); and a decision about where shared fixtures will live — a conftest.py at the top of the test tree.
The test plan. Three cases, each of which must pass alone and in any order:
| case | input | expected | |---|---|---| | fresh database | nothing inserted | COUNT(*) is 0 | | one insert | 'asha' | COUNT(*) is 1 | | name round-trip | 'ravi' | the first name read back is 'ravi' |
The setup plan. Then the same kind of table for what the tests stand on:
| resource | cost to build | does a test change it? | scope | |---|---|---|---| | connection + table | slow (half a second) | no | wide — module or session | | the rows in the table | free | yes, every insert | function — reset after each test |
Read the last column carefully: one object, the connection, holds both a slow thing and a changing thing. That is why the chapter ends up with two fixtures — one wide, one narrow — instead of one.
What not to test. Not sqlite3 itself; it is tested far more thoroughly than your code will ever be. Not the fixtures directly — a fixture is checked by the tests that use it, and by reading --setup-show. And not the half-second delay: speed is something you measure with --durations, not something you assert.
scope= — once per what?
A fixture's scope says how long one value lives before pytest throws it away and builds another. The default is function: one per test. Change one line:
@pytest.fixture(scope="module")
def db():
conn = connect()
yield conn
conn.close()pytest -q:
..F [100%]
=================================== FAILURES ===================================
_____________________________ test_names_are_text ______________________________
db = <sqlite3.Connection object at 0x79350aa45c60>
def test_names_are_text(db):
db.execute("INSERT INTO users VALUES ('ravi')")
name = db.execute("SELECT name FROM users").fetchone()[0]
> assert name == "ravi"
E AssertionError: assert 'asha' == 'ravi'
E
E - ravi
E + asha
test_users.py:35: AssertionError
=========================== short test summary info ============================
FAILED test_users.py::test_names_are_text - AssertionError: assert 'asha' == ...
1 failed, 2 passed in 0.52sTwo things happened. The run went from 1.65 seconds to 0.52 — one connection instead of three. And a test that passed a moment ago now fails. Keep that failure in mind; it gets its own section below. First, the scopes themselves.
There are five, from narrowest to widest:
| scope | one value per | torn down after | |---|---|---| | function | test (the default) | each test | | class | test class | the last test in the class | | module | test file | the last test in the file | | package | directory where the fixture is defined | the last test in that directory, sub-directories included | | session | whole pytest run | the last test of the run |
A counter makes the difference visible. Each fixture below counts how many times it was built:
test_scopes.py:
from collections import Counter
import pytest
made = Counter()
@pytest.fixture(scope="session")
def per_session():
made["session"] += 1
@pytest.fixture(scope="module")
def per_module():
made["module"] += 1
@pytest.fixture(scope="class")
def per_class():
made["class"] += 1
@pytest.fixture
def per_test():
made["function"] += 1
class TestFirst:
def test_a(self, per_session, per_module, per_class, per_test):
pass
def test_b(self, per_session, per_module, per_class, per_test):
pass
class TestSecond:
def test_c(self, per_session, per_module, per_class, per_test):
pass
def test_report():
print(dict(made))pytest -q -s (the -s lets print through):
...{'session': 1, 'module': 1, 'class': 2, 'function': 3}
.
4 passed in 0.01sThree tests used the fixtures. The function fixture was built three times, the class fixture twice (two classes), and the module and session fixtures once each.
Watching it happen: --setup-show
Counting works, but pytest can draw the whole timeline for you. Run the same file with pytest -q --setup-show:
SETUP S per_session
SETUP M per_module
SETUP C per_class
SETUP F per_test
test_scopes.py::TestFirst::test_a (fixtures used: per_class, per_module, per_session, per_test) .
TEARDOWN F per_test
SETUP F per_test
test_scopes.py::TestFirst::test_b (fixtures used: per_class, per_module, per_session, per_test) .
TEARDOWN F per_test
TEARDOWN C per_class
SETUP C per_class
SETUP F per_test
test_scopes.py::TestSecond::test_c (fixtures used: per_class, per_module, per_session, per_test) .
TEARDOWN F per_test
TEARDOWN C per_class
test_scopes.py::test_report .
TEARDOWN M per_module
TEARDOWN S per_session
4 passed in 0.01sThe letter after SETUP is the scope — Session, Package, Module, Class, Function — and the indentation nests them. Read it top to bottom and you can see exactly when each value is born and when it dies. Whenever you are unsure what a fixture is doing, this is the first flag to reach for.
A shared fixture shares its state
Back to the failure. With scope="module" all three tests received the same connection. test_insert_one added 'asha' and nothing removed her. Then test_names_are_text added 'ravi', asked for the first name, and got 'asha'.
The clearest sign of this bug is a test that passes on its own. pytest -q test_users.py::test_names_are_text:
. [100%]
1 passed in 0.50sAlone it passes; in company it fails. Tests that depend on what ran before them are worse than slow tests: they fail in one order and pass in another, and they waste an afternoon each time.
The rule: widen the scope of the expensive thing, keep the scope of the state narrow. Split the fixture in two — a wide one that owns the connection, and a function-scoped one that cleans up after each test:
@pytest.fixture(scope="module")
def connection():
conn = connect()
yield conn
conn.close()
@pytest.fixture
def db(connection):
yield connection
connection.rollback() # undo whatever this test wroteThe tests do not change — they still ask for db. pytest -q --durations=3:
... [100%]
============================= slowest 3 durations ==============================
0.50s setup test_users.py::test_starts_empty
(2 durations < 0.005s hidden. Use -vv to show these durations.)
3 passed in 0.51sOne connection, three passing tests. sqlite3 opens a transaction on the first INSERT, and rollback() throws the whole transaction away, so every test starts with an empty table.
Why put the cleanup in a fixture, rather than starting each test with DELETE FROM users? Because a cleanup that every test has to remember is a cleanup that one test will forget — and the test that forgets is never the one that fails; the next one is. In the fixture, the cleanup happens after every test that asks for db, including the tests written next year by someone who has never read this file. And why rollback() rather than DELETE? A rollback undoes everything the test wrote, in every table, without the fixture having to know what those writes were.
--setup-show shows the split:
SETUP M connection
SETUP F db (fixtures used: connection)
test_users.py::test_starts_empty (fixtures used: connection, db) .
TEARDOWN F db
SETUP F db (fixtures used: connection)
test_users.py::test_insert_one (fixtures used: connection, db) .
TEARDOWN F db
SETUP F db (fixtures used: connection)
test_users.py::test_names_are_text (fixtures used: connection, db) .
TEARDOWN F db
TEARDOWN M connection
3 passed in 0.51sA function-scoped fixture may freely request a wider one, as db requests connection here. The opposite direction is not allowed.
ScopeMismatch — wide may not ask for narrow
test_mismatch.py:
import sqlite3
import pytest
@pytest.fixture
def db_name():
return ":memory:"
@pytest.fixture(scope="session")
def connection(db_name):
conn = sqlite3.connect(db_name)
yield conn
conn.close()
def test_connects(connection):
assert connection.execute("SELECT 1").fetchone() == (1,)pytest -q:
E [100%]
==================================== ERRORS ====================================
_______________________ ERROR at setup of test_connects ________________________
ScopeMismatch: You tried to access the function scoped fixture db_name with a session scoped request object. Requesting fixture stack:
test_mismatch.py:11: def connection(db_name)
Requested fixture:
test_mismatch.py:6: def db_name()
=========================== short test summary info ============================
ERROR test_mismatch.py::test_connects - Failed: ScopeMismatch: You tried to a...
1 error in 0.01sThink about what the request would mean. connection lives for the whole run; db_name is thrown away after the first test. The session value would be holding on to something that, by its own rules, no longer exists. So pytest refuses — and note that it is an Error at setup, not a Failure: the test never ran.
The message names both fixtures and both lines. The fix is always the same: make the requested fixture at least as wide as the one asking for it — here @pytest.fixture(scope="session") on db_name — or make the asking fixture narrower.
conftest.py — fixtures without imports
Once a second test file needs db, copying the fixture is the wrong answer. Move it into a file named exactly conftest.py. Pytest loads that file by itself, and every test in the same directory and below it can request its fixtures — with no import.
A project with a sub-directory:
conftest.py
test_users.py
reports/
conftest.py
test_reports.pyconftest.py:
import sqlite3
import time
import pytest
def connect():
time.sleep(0.5) # stands in for a slow server handshake
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE users (name TEXT, city TEXT)")
return conn
@pytest.fixture(scope="session")
def connection():
"""One database connection, shared by the whole run."""
conn = connect()
yield conn
conn.close()
@pytest.fixture
def db(connection):
"""The shared connection; every write is rolled back after the test."""
yield connection
connection.rollback()test_users.py — no imports at all:
def test_starts_empty(db):
count = db.execute("SELECT COUNT(*) FROM users").fetchone()[0]
assert count == 0
def test_insert_one(db):
db.execute("INSERT INTO users VALUES ('asha', 'Dhaka')")
count = db.execute("SELECT COUNT(*) FROM users").fetchone()[0]
assert count == 1The scope is now session: one connection for every file in the project.
Overriding a fixture for one directory
The tests in reports/ all want a database that already has users in it. A conftest.py in that directory can define a fixture with the same name; for tests in reports/ the nearer definition wins. And if it requests its own name, it receives the parent's version:
reports/conftest.py:
import pytest
@pytest.fixture
def db(db):
"""The parent db, with three users already in it."""
db.executemany(
"INSERT INTO users VALUES (?, ?)",
[("asha", "Dhaka"), ("ravi", "Pune"), ("omar", "Cairo")],
)
return dbreports/test_reports.py:
def test_user_count(db):
count = db.execute("SELECT COUNT(*) FROM users").fetchone()[0]
assert count == 3
def test_cities_sorted(db):
rows = db.execute("SELECT city FROM users ORDER BY city").fetchall()
assert [city for (city,) in rows] == ["Cairo", "Dhaka", "Pune"]pytest -v:
============================= test session starts ==============================
collecting ... collected 4 items
reports/test_reports.py::test_user_count PASSED [ 25%]
reports/test_reports.py::test_cities_sorted PASSED [ 50%]
test_users.py::test_starts_empty PASSED [ 75%]
test_users.py::test_insert_one PASSED [100%]
============================== 4 passed in 0.51s ===============================Look at the order. The reports/ tests ran first and inserted three users each time — yet test_starts_empty, which ran after them, still found an empty table. The parent db rolled back the seeded rows too, because the override is built on top of it. --setup-show shows the two layers, both named db:
SETUP S connection
SETUP F db (fixtures used: connection)
SETUP F db (fixtures used: db)
reports/test_reports.py::test_user_count (fixtures used: connection, db) .
TEARDOWN F db
TEARDOWN F db(That is the first test; the rest repeat the pattern.) The override only reaches downward: tests in the top directory never see it.
--fixtures — what can I ask for here?
With fixtures spread over several conftest.py files, you need a way to see what a given test can request. pytest --fixtures reports lists everything visible from that directory. The list starts with pytest's built-in fixtures (chapter nine) and those of installed plugins; yours come at the end:
------------------------ fixtures defined from conftest ------------------------
connection [session scope] -- conftest.py:15
One database connection, shared by the whole run.
db -- conftest.py:23
The shared connection; every write is rolled back after the test.
db -- reports/conftest.py:5
The parent db, with three users already in it.Each entry gives the name, the scope (when it is not function), the file and line, and the first lines of the docstring. That last part is the reason to write a one-line docstring on every shared fixture: it is the documentation people will actually read.
autouse=True — and what it costs
A fixture marked autouse=True is used by every test in its reach, whether the test asks for it or not. Added to the top conftest.py, this one checks that no test leaves rows behind:
@pytest.fixture(autouse=True)
def no_rows_left(connection):
"""After every test, check that the users table is empty again."""
yield
count = connection.execute("SELECT COUNT(*) FROM users").fetchone()[0]
assert count == 0, f"{count} row(s) left behind"It works — a test that writes through connection directly and skips the rollback is caught at teardown. But now add a test that has nothing to do with databases:
test_text.py:
def slugify(title):
return title.strip().lower().replace(" ", "-")
def test_slugify():
assert slugify(" Hello World ") == "hello-world"pytest -q --setup-show test_text.py:
SETUP S connection
SETUP F no_rows_left (fixtures used: connection)
test_text.py::test_slugify (fixtures used: connection, no_rows_left) .
TEARDOWN F no_rows_left
TEARDOWN S connection
1 passed in 0.50sA one-line string test now opens a database and takes half a second. That is the cost of autouse: it is invisible — nothing in test_slugify says a database is involved — and it is unconditional — it runs for every test below its conftest.py, including the ones that do not need it. Keep autouse for things that are cheap and truly universal, and put it in the narrowest conftest.py that covers the tests that need it.
Which fixture is created first?
When a test needs several fixtures, pytest orders them by three rules:
- Wider scope first. Session before module before function, whatever the order in the test's signature.
- Within a scope,
autousefixtures first. - Dependencies before the fixtures that need them.
test_order.py:
import pytest
log = []
@pytest.fixture(scope="session")
def server():
log.append("server")
@pytest.fixture(scope="module")
def connection(server):
log.append("connection")
@pytest.fixture
def user():
log.append("user")
@pytest.fixture
def cart(user):
log.append("cart")
@pytest.fixture(autouse=True)
def audit():
log.append("audit")
def test_order(cart, connection):
print(log)pytest -q -s:
['server', 'connection', 'audit', 'user', 'cart']
.
1 passed in 0.01sThe test asked for cart before connection, but connection — module scope — was built first, after its own dependency server. Among the function-scoped fixtures, the autouse audit came first, then user, which cart needs. Teardown runs in exactly the reverse order. Beyond these rules, do not rely on order: if one fixture must exist before another, make it a dependency by naming it as a parameter.
A complete example
A small shop module, tested against one shared in-memory database.
shop.py:
def add_order(conn, customer, total):
conn.execute(
"INSERT INTO orders (customer, total) VALUES (?, ?)", (customer, total)
)
def revenue(conn):
return conn.execute("SELECT COALESCE(SUM(total), 0) FROM orders").fetchone()[0]
def top_customer(conn):
row = conn.execute(
"SELECT customer FROM orders GROUP BY customer "
"ORDER BY SUM(total) DESC LIMIT 1"
).fetchone()
return row[0] if row else Noneconftest.py:
import sqlite3
import time
import pytest
@pytest.fixture(scope="session")
def connection():
"""One in-memory database with the orders table, for the whole run."""
time.sleep(0.5) # stands in for a slow server handshake
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE orders (customer TEXT, total REAL)")
yield conn
conn.close()
@pytest.fixture
def db(connection):
"""The shared connection; whatever a test writes is rolled back."""
yield connection
connection.rollback()test_orders.py:
from shop import add_order, revenue, top_customer
def test_revenue_starts_at_zero(db):
assert revenue(db) == 0
def test_add_order_counts_toward_revenue(db):
add_order(db, "asha", 250.0)
assert revenue(db) == 250.0
def test_no_top_customer_without_orders(db):
assert top_customer(db) is Nonereports/conftest.py:
import pytest
from shop import add_order
@pytest.fixture
def db(db):
"""The parent db, with four orders from three customers already in it."""
add_order(db, "asha", 250.0)
add_order(db, "ravi", 900.0)
add_order(db, "omar", 120.0)
add_order(db, "asha", 700.0)
return dbreports/test_reports.py:
from shop import revenue, top_customer
def test_total_revenue(db):
assert revenue(db) == 1970.0
def test_top_customer_by_total(db):
assert top_customer(db) == "asha"pytest -v --durations=1:
============================= test session starts ==============================
collecting ... collected 5 items
reports/test_reports.py::test_total_revenue PASSED [ 20%]
reports/test_reports.py::test_top_customer_by_total PASSED [ 40%]
test_orders.py::test_revenue_starts_at_zero PASSED [ 60%]
test_orders.py::test_add_order_counts_toward_revenue PASSED [ 80%]
test_orders.py::test_no_top_customer_without_orders PASSED [100%]
============================= slowest 1 durations ==============================
0.50s setup reports/test_reports.py::test_total_revenue
============================== 5 passed in 0.51s ===============================Three things are worth noticing.
First, the slow setup happened once, on the first test of the run, and nowhere else. That is the session scope paying off.
Second, the tests in test_orders.py ran after the report tests had inserted eight orders in total, and still saw zero revenue. Replace connection.rollback() with pass and run again, and the price of shared state shows up immediately:
FAILED test_orders.py::test_revenue_starts_at_zero - assert 3940.0 == 0
FAILED test_orders.py::test_add_order_counts_toward_revenue - assert 4190.0 =...
FAILED test_orders.py::test_no_top_customer_without_orders - AssertionError: ...
3 failed, 2 passed in 0.52sThird, no test file imports a fixture. test_orders.py imports shop, the code under test, and nothing else; the fixtures arrive by name from the nearest conftest.py.
When it breaks
ScopeMismatch: You tried to access the function scoped fixture db_name with a session scoped request object. A wide fixture requested a narrower one. Widen the requested fixture (db_name here) to at least the scope of the one asking, or narrow the one asking. The two lines under Requesting fixture stack tell you which is which.
AssertionError: assert 'asha' == 'ravi' — but the test passes when run on its own State is leaking between tests through a fixture wider than function. Run with --setup-show to see which fixture is shared, then split it: keep the expensive object wide and add a function-scoped fixture that undoes each test's changes.
fixture 'report_title' not found The fixture exists, but not where this test can see it. A conftest.py only serves its own directory and the directories below it — a fixture in reports/conftest.py is invisible to a test in the directory above. Move it up to a conftest.py that covers both. pytest --fixtures path/to/test_file.py shows exactly what that file can see.
Rows from one test appear in another even though db rolls back Something called commit(). A rollback only discards what has not been committed, so code that commits — or a test that uses connection directly instead of db — writes permanently. A guard like the no_rows_left fixture turns that into a visible ERROR at teardown ... AssertionError: 1 row(s) left behind.
A test that touches no database has become slow An autouse fixture somewhere above it depends on the expensive one. pytest --setup-show on that test lists every fixture it really used, including the ones it never asked for.
from conftest import db in a test file Do not import from conftest.py. Pytest already gives you its fixtures by name, and import conftest gets whichever conftest.py Python happens to find first. In the project above, adding that line to the top-level test_users.py imported the one in reports/ — the seeding override — and test_starts_empty failed with assert 3 == 0. An imported fixture also counts as defined in the test file itself, which beats every conftest.py. Request fixtures by naming them as parameters, and nothing else.
Step 4 of 6 — Predict
Check your understanding
Run with pytest -q -s. What does test_report print?
import pytest
calls = {"module": 0, "function": 0}
@pytest.fixture(scope="module")
def conn():
calls["module"] += 1
@pytest.fixture
def row(conn):
calls["function"] += 1
def test_a(row):
pass
def test_b(row):
pass
def test_c(conn):
pass
def test_report():
print(calls)- A{'module': 3, 'function': 3}
- B{'module': 1, 'function': 3}
- C{'module': 1, 'function': 2}
- D{'module': 3, 'function': 2}
This test errors with ScopeMismatch before it runs. What is the smallest correct fix?
import pytest
@pytest.fixture
def settings():
return {"db": ":memory:"}
@pytest.fixture(scope="session")
def connection(settings):
return settings["db"]
def test_connects(connection):
assert connection == ":memory:"- APut `test_connects` inside a class
- BGive `settings` `@pytest.fixture(scope="session")`
- CAdd `autouse=True` to `connection`
- DMove `settings` below `connection` in the file
Both conftest.py files define db. Which db does test_count in reports/test_reports.py receive?
# conftest.py
@pytest.fixture
def db(connection):
yield connection
connection.rollback()
# reports/conftest.py
@pytest.fixture
def db(db):
db.execute("INSERT INTO users VALUES ('asha', 'Dhaka')")
return db
# reports/test_reports.py
def test_count(db):
...- AThe top-level one, because it is loaded first
- BNeither — two fixtures with the same name is an error
- CThe `reports/` one, but its `db` parameter receives itself, so it recurses forever
- DThe `reports/` one, which receives the top-level one and adds a row on top of it
Answering needs an account
Sign in to check your answers
The questions are above, and working them out in your head is the part that matters. Sign in to see the answers, the explanations and the three-level hints.
Your turn
Build a small project around a library.py with two functions: add_book(conn, title, author) and books_by(conn, author), which returns a sorted list of titles.
- In a top-level
conftest.py, write a session-scopedconnectionfixture that openssqlite3.connect(":memory:"), creates abooks (title TEXT, author TEXT)table, and sleeps for half a second to stand in for a slow server. Give it a docstring. - Add a function-scoped
dbfixture on top of it that rolls back after every test. - Write
test_library.pywith three tests that each add books and checkbooks_by. Runpytest -q --durations=3and confirm the half-second appears once. - Create
search/conftest.pythat overridesdbto start with five books already in it, andsearch/test_search.pywith two tests that rely on them. - Run
pytest --setup-showand find the twodblayers in the output for thesearch/tests. Then runpytest --fixtures searchand check that both docstrings appear.
Then break it on purpose, twice. First, replace the rollback() line with pass and run the whole suite — which tests fail, and does each one pass when run alone? Second, give connection a function-scoped fixture db_name to depend on, and read the ScopeMismatch message line by line.
Those five minutes are the chapter. Scope is a speed decision, but the bugs it creates are isolation bugs — and the only way to learn to spot them is to cause a few yourself.
Solution
The layout:
library.py
conftest.py
test_library.py
search/
conftest.py
test_search.pylibrary.py:
def add_book(conn, title, author):
conn.execute("INSERT INTO books VALUES (?, ?)", (title, author))
def books_by(conn, author):
rows = conn.execute(
"SELECT title FROM books WHERE author = ? ORDER BY title", (author,)
).fetchall()
return [title for (title,) in rows]conftest.py:
import sqlite3
import time
import pytest
@pytest.fixture(scope="session")
def connection():
"""One in-memory database with the books table, for the whole run."""
time.sleep(0.5) # stands in for a slow server handshake
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE books (title TEXT, author TEXT)")
yield conn
conn.close()
@pytest.fixture
def db(connection):
"""The shared connection; every test's writes are rolled back."""
yield connection
connection.rollback()test_library.py:
from library import add_book, books_by
def test_unknown_author_has_no_books(db):
assert books_by(db, "Nobody") == []
def test_one_book(db):
add_book(db, "Gitanjali", "Tagore")
assert books_by(db, "Tagore") == ["Gitanjali"]
def test_titles_come_back_sorted(db):
add_book(db, "Kabuliwala", "Tagore")
add_book(db, "Gitanjali", "Tagore")
assert books_by(db, "Tagore") == ["Gitanjali", "Kabuliwala"]search/conftest.py:
import pytest
from library import add_book
@pytest.fixture
def db(db):
"""The parent db, with five books by three authors already in it."""
add_book(db, "Gitanjali", "Tagore")
add_book(db, "Kabuliwala", "Tagore")
add_book(db, "Godaan", "Premchand")
add_book(db, "Nirmala", "Premchand")
add_book(db, "Palace Walk", "Mahfouz")
return dbsearch/test_search.py:
from library import books_by
def test_two_books_by_premchand(db):
assert books_by(db, "Premchand") == ["Godaan", "Nirmala"]
def test_author_match_is_exact(db):
assert books_by(db, "tagore") == []pytest -q --durations=3:
..... [100%]
============================= slowest 3 durations ==============================
0.50s setup search/test_search.py::test_two_books_by_premchand
(2 durations < 0.005s hidden. Use -vv to show these durations.)
5 passed in 0.51spytest -q --setup-show search shows, for each of the two tests, the same picture as in the chapter: SETUP F db (fixtures used: connection) followed by SETUP F db (fixtures used: db) — the parent layer, then the override on top of it. pytest --fixtures search lists connection with [session scope], both db fixtures with their file and line, and all three docstrings.
The first experiment — rollback() replaced with pass — gives:
FAILED test_library.py::test_one_book - AssertionError: assert ['Gitanjali',....
FAILED test_library.py::test_titles_come_back_sorted - AssertionError: assert...
2 failed, 3 passed in 0.52sand pytest -q test_library.py::test_one_book on its own:
. [100%]
1 passed in 0.50sThe search/ tests ran first and left five books behind, so test_one_book found two Tagore titles instead of one. Alone, it passes. That is the signature of leaked state, exactly as in the chapter. The second experiment turns all five tests into E, each with the same ScopeMismatch you met earlier, now pointing at conftest.py: def connection(db_name) is the requester, def db_name() the fixture that is too narrow.
Why each decision:
connectionis session-scoped because it is the only slow thing, and nothing a test does changes the connection itself — only the rows in it.dbis function-scoped because the rows do change. Its whole job is the rollback after each test; the tests never need to know it exists beyond its name.- The tests request
db, neverconnection. A test that tookconnectiondirectly would skip the rollback and leak its rows into the next test. - The
search/override requestsdb, notconnection, so the seeded books become part of the test's own work and the parent's rollback removes them with everything else. If the cleanup indbever changes, the override inherits the change without being touched. test_author_match_is_exactis in the plan on purpose: it pins down that"tagore"is not"Tagore", a boundary someone will one day want to change — and should change on purpose, with a failing test telling them so.- Every shared fixture has a docstring, because
pytest --fixturesis how the next person will discover what they can ask for.
Step 6 of 6
Stretch — the chapter quiz
Ten questions from easy to hard. The last ones are difficult on purpose.
Sign in to take the quiz