Testing with Fixtures
Goals
- Understand why putting a fixture in one test file makes it unavailable to other test files.
- Use
conftest.pyto define fixtures that pytest shares automatically across all test files in a directory. - Split tests into multiple files, each focused on one part of the application.
The Duplication Problem
The previous lesson put
small_dbdirectly intest_server_db.py. What happens if you want to write a second test file that also needs a small database?
- A fixture defined inside a test file is private to that file
- Importing it from another test file would work, but it breaks pytest's fixture discovery
- Copying the fixture into every new test file means maintaining the same setup code in multiple places
- If the database schema changes, every copy has to be updated separately
- The fixture is doing real work: creating a temporary database, inserting rows, and returning a path
- That work belongs in one place, shared by all the tests that need it
Where does pytest look when it needs a fixture that is not defined in the current test file?
- pytest looks for a file named
conftest.pyin the same directory as the test file, then in each parent directory up to the project root- No import statement is required: fixtures defined in
conftest.pyare available to every test file in the same directory and all subdirectories - pytest finds
conftest.pyby convention, not by any explicit reference in the test files
- No import statement is required: fixtures defined in
Extracting the Fixture
Move the
SMALLlist, the SQL constants, and thesmall_dbfixture fromtest_server_db.pyinto a new file calledconftest.pyin the same directory.
- Everything the fixture needs moves with it:
SMALL,CREATE_TABLE, andINSERT_ROW- Test files that use
small_dbdo not import anything fromconftest.py; pytest injects the fixture automatically based on the parameter name
- Test files that use
- The fixture itself is unchanged: it still uses
tmp_pathto get a fresh directory, creates the database, inserts the two rows, and returns the path
import sqlite3
import pytest
from schema import CREATE_TABLE
from small import SMALL
INSERT_ROW = "insert into sightings values (?, ?, ?, ?, ?, ?, ?, ?)"
@pytest.fixture
def small_db(tmp_path):
db_path = tmp_path / "test.db"
conn = sqlite3.connect(db_path)
conn.execute(CREATE_TABLE)
for s in SMALL:
conn.execute(
INSERT_ROW,
[
s["id"],
s["species"],
s["sex"],
s["weight"],
s["color"],
s["datetime"],
s["latitude"],
s["longitude"],
],
)
conn.commit()
conn.close()
return db_path
Testing in Separate Files
Write a file called
test_index.pywith tests that check the index route, and a file calledtest_detail.pywith tests that check the detail route. Both files should usesmall_db.
test_index.pyimports onlyTestClientandmake_appsmall_dbarrives as a parametertest_index_okchecks that the home page responds with 200 and contains the site titletest_index_shows_both_speciesconfirms both species fromSMALLappear in the page
test_detail.pyalso imports onlyTestClientandmake_apptest_detail_okvisits sighting 1 and confirms the species name appearstest_missing_sightingconfirms that a nonexistent ID returns 404test_none_displayed_as_emptyconfirms that a sighting with null fields does not show the word"None"in the page
from litestar.testing import TestClient
from server_db import make_app
def test_index_ok(small_db):
with TestClient(app=make_app(small_db)) as client:
response = client.get("/")
assert response.status_code == 200
assert "Sasquatch Sightings" in response.text
def test_index_shows_both_species(small_db):
with TestClient(app=make_app(small_db)) as client:
response = client.get("/")
assert "G. canadensis" in response.text
assert "G. horribilus" in response.text
from litestar.testing import TestClient
from server_db import make_app
def test_detail_ok(small_db):
with TestClient(app=make_app(small_db)) as client:
response = client.get("/sighting/1")
assert response.status_code == 200
assert "G. canadensis" in response.text
def test_missing_sighting(small_db):
with TestClient(app=make_app(small_db)) as client:
response = client.get("/sighting/9999")
assert response.status_code == 404
def test_none_displayed_as_empty(small_db):
with TestClient(app=make_app(small_db)) as client:
response = client.get("/sighting/2")
assert response.status_code == 200
assert "None" not in response.text
- Run both files together from the
testdatabase/directory:
pytest test_index.py test_detail.py -v
- pytest loads
conftest.pyonce and makessmall_dbavailable to every test in both files - Each test that lists
small_dbas a parameter gets its own fresh database, so a write in one test cannot affect another
Check Understanding
See [pytest2025] for the full pytest documentation,
including the reference page on fixtures and conftest.py.
The code below causes an error even though small_db is defined in conftest.py. What is wrong?
pytest injects fixtures automatically by matching parameter names.
Importing a fixture function and passing it as a parameter bypasses that mechanism.
pytest no longer recognizes it as a fixture and raises an error.
Remove the import line and let pytest inject small_db on its own.
from conftest import small_db
def test_index_ok(small_db):
...
Each test that uses small_db gets its own database. What would happen if they all shared one database instead?
Tests that add or delete rows would affect every test that runs afterward. The order in which pytest runs tests would determine whether they pass or fail, which makes failures hard to reproduce and debug. Keeping each test isolated means it can be run alone or in any order and still produce the same result.
The test below always passes, even when it should not. What is wrong?
status_code != 200 passes for any code other than 200, including 500 (server error).
A bug that causes the server to crash would make this test pass when it should fail.
The assertion should be assert response.status_code == 404 to confirm the right kind of failure.
def test_missing_sighting(small_db):
with TestClient(app=make_app(small_db)) as client:
response = client.get("/sighting/9999")
assert response.status_code != 200
Exercises
Add a Test for the CSS Route
Add a test to test_index.py that sends GET /style.css.
It must check that the response status code is 200 and that the response text contains "table".
Test a Specific Detail
Add a test to test_detail.py that visits /sighting/2
and confirms that "G. horribilus" appears in the response.
Add a Third Test File
Create test_links.py that uses small_db to check that
the index page contains two links inside the table
and that the detail page for sighting 1 contains a link whose text is "Back to all sightings".
You will need to look for the right strings in response.text.
Change the Test Data
Add a third row to SMALL in conftest.py with a species of "G. canadensis" and a different color.
Without changing any test assertions, run the suite and confirm that all tests still pass,
then explain why test_index_shows_both_species still passes even though the number of rows changed.
Confirm Isolation
Add a test to test_detail.py that inserts a third row directly into small_db using sqlite3,
then checks that the index page shows three links.
Run the full suite and confirm that test_index_ok still passes with only two links,
which shows that each test received its own database.