Two tests for a user table each open a database, create the table and add
two users before they check anything. Save this as test_users.py and run
pytest:
import sqlite3
def test_count_users() -> None:
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE users (name TEXT)")
db.executemany("INSERT INTO users VALUES (?)", [("ada",), ("grace",)])
assert db.execute("SELECT count(*) FROM users").fetchone() == (2,)
db.close()
def test_find_user() -> None:
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE users (name TEXT)")
db.executemany("INSERT INTO users VALUES (?)", [("ada",), ("grace",)])
assert db.execute("SELECT name FROM users").fetchone() == ("ada",)
db.close()
Both tests repeat the same three lines of setup. And when an assert
fails, the test stops there, so db.close() never runs.
Move the setup into a fixture
A fixture is a function that prepares something your tests need. You
mark it with @pytest.fixture. A test asks for a fixture by naming it as a
parameter, and pytest calls the fixture and passes in the result.
import sqlite3
from collections.abc import Iterator
import pytest
@pytest.fixture
def db() -> Iterator[sqlite3.Connection]:
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE users (name TEXT)")
conn.executemany("INSERT INTO users VALUES (?)", [("ada",), ("grace",)])
yield conn
conn.close()
def test_count_users(db: sqlite3.Connection) -> None:
assert db.execute("SELECT count(*) FROM users").fetchone() == (2,)
def test_find_user(db: sqlite3.Connection) -> None:
assert db.execute("SELECT name FROM users").fetchone() == ("ada",)
pytest runs the fixture up to yield, and the test receives the value it
yields. The code after yield is the teardown, the cleanup that runs
after the test. pytest runs it whether the test passes or fails. Each test
gets a fresh connection of its own.
A fixture with yield is a generator, so its return type is
Iterator[sqlite3.Connection].
Let fixtures ask for other fixtures
A fixture can ask for another fixture through its parameters, the same way a
test does. pytest sets up the dependency first. After the test, it tears
the fixtures down in reverse order. Run this with pytest -s, which shows
the prints:
import sqlite3
from collections.abc import Iterator
import pytest
@pytest.fixture
def conn() -> Iterator[sqlite3.Connection]:
print("open connection")
conn = sqlite3.connect(":memory:")
yield conn
print("close connection")
conn.close()
@pytest.fixture
def users(conn: sqlite3.Connection) -> Iterator[sqlite3.Connection]:
print("create table")
conn.execute("CREATE TABLE users (name TEXT)")
yield conn
print("drop table")
conn.execute("DROP TABLE users")
def test_starts_empty(users: sqlite3.Connection) -> None:
print("run test")
assert users.execute("SELECT count(*) FROM users").fetchone() == (0,)
open connection
create table
run test
drop table
close connection
The users teardown drops the table while the connection is still open.
pytest makes each fixture once per test, so a test that asks for both
conn and users gets one connection.
pytest sets the order from dependencies, scope and autouse. Fixture names
and the order of a test's parameters do not count. If one fixture must run
before another, make it a dependency.
Give each resource its own fixture
Suppose one fixture opens a database and then starts a worker, and the
worker fails to start. The fixture never reaches yield, so its teardown
never runs and the database stays open. Split it into a database fixture
and a worker fixture that asks for it. When worker fails, pytest still
closes the database, because that fixture finished its setup.
Choose a scope
A fixture's scope sets how long pytest keeps its value before it runs
the teardown. The default, "function", makes a new value for every test.
scope="module" makes one for all the tests in a file, and
scope="session" makes one for the whole run. There are also "class" and
"package" scopes.
A wider scope saves time when setup is slow, but the tests then share one object. If one test adds a row, the next test sees it, so a test can pass or fail depending on which tests ran first. Give the slow part a wide scope, and reset the shared state in a function-scoped fixture:
import sqlite3
from collections.abc import Iterator
import pytest
@pytest.fixture(scope="module")
def shared_conn() -> Iterator[sqlite3.Connection]:
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE users (name TEXT)")
yield conn
conn.close()
@pytest.fixture
def db(shared_conn: sqlite3.Connection) -> Iterator[sqlite3.Connection]:
yield shared_conn
shared_conn.execute("DELETE FROM users")
def test_add_user(db: sqlite3.Connection) -> None:
db.execute("INSERT INTO users VALUES ('ada')")
assert db.execute("SELECT count(*) FROM users").fetchone() == (1,)
def test_starts_empty(db: sqlite3.Connection) -> None:
assert db.execute("SELECT count(*) FROM users").fetchone() == (0,)
A module-scoped fixture cannot ask for a function-scoped one, because pytest
would tear down the function-scoped value after the first test. pytest
reports a ScopeMismatch error.
Check your understanding
A test fails at its assert. Does the code after yield in its fixture still run?
Show answer
Hide answer
Yes. pytest runs the teardown after every test that used the fixture, pass
or fail. It skips only the teardown of a fixture that raised before it
reached yield. That is why each resource gets its own fixture, so that
pytest can still tear down the ones that finished their setup.
Return a function when tests need different data
A factory fixture returns a function that builds what the test needs. Each test calls it as often as it likes, with its own data, and the fixture cleans up everything it made:
import sqlite3
from collections.abc import Callable, Iterator
import pytest
@pytest.fixture
def make_db() -> Iterator[Callable[[list[str]], sqlite3.Connection]]:
opened: list[sqlite3.Connection] = []
def make(names: list[str]) -> sqlite3.Connection:
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE users (name TEXT)")
conn.executemany("INSERT INTO users VALUES (?)", [(n,) for n in names])
opened.append(conn)
return conn
yield make
for conn in opened:
conn.close()
def test_databases_are_separate(
make_db: Callable[[list[str]], sqlite3.Connection],
) -> None:
empty = make_db([])
full = make_db(["ada", "grace"])
assert empty.execute("SELECT count(*) FROM users").fetchone() == (0,)
assert full.execute("SELECT count(*) FROM users").fetchone() == (2,)
Keep autouse fixtures few
@pytest.fixture(autouse=True) runs a fixture for every test in its file
without the tests asking for it. Use it for setup that every test needs,
such as clearing a module-level cache. Keep such fixtures few, because a
test's parameters no longer show all of its setup.
The next exercise has you work out fixture setup and teardown order yourself.
Going further
request.addfinalizer(fn) registers a cleanup function from inside a
fixture. If you register several, pytest runs them in reverse order. contextlib.ExitStack does
the same job in a yield fixture. pytest's guide How to use fixtures
covers each feature, and the fixtures reference
lists the ordering rules under "Fixture instantiation order".