Testing with Fixtures

Goals

The Duplication Problem

The previous lesson put small_db directly in test_server_db.py. What happens if you want to write a second test file that also needs a small database?

Where does pytest look when it needs a fixture that is not defined in the current test file?

Extracting the Fixture

Move the SMALL list, the SQL constants, and the small_db fixture from test_server_db.py into a new file called conftest.py in the same directory.

i
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.py with tests that check the index route, and a file called test_detail.py with tests that check the detail route. Both files should use small_db.

i
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
i
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
i
pytest test_index.py test_detail.py -v

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.