Skip to content

Tests

A module’s tests live in the module. Not in a global folder, not next to the kernel: in modules/<your module>/tests/, with the code they describe.

Nothing to configure: modules is already one of the paths pytest walks.

The detail that is not optional

modules/library/
tests/
__init__.py ← this one
test_intake.py
test_loans.py

Without that empty file, pytest imports your tests under their bare name. The first module gets away with it; the second one to name a file test_models.py fails collection for the whole suite, not only its own:

import file mismatch:
imported module 'test_models' has this __file__ attribute:
modules/pipeline/tests/test_models.py
which is not the same as the test file we want to collect:
modules/library/tests/test_models.py

The failure lands on whoever arrives second, far from its cause. A kernel test now checks that every module tests/ folder carries its __init__.py.

Three levels, three different questions

What it answersWhat it touches
Unitis my logic right?nothing: no database, no network, no queue
Integrationdoes my module hold against its real dependencies?a database, the registry, the state machine
End to enddoes the user’s journey work?HTTP → service → ORM → database

None replaces another. A green unit test on a function nobody calls proves nothing; an integration test does not tell you why a computation is wrong.

Unit: the logic, alone

The library module reads a delivery note and pulls ISBNs out of it. Text in, ISBNs out: so it is tested with text.

"""Reading a delivery note: the ISBNs, and the ones we refuse to trust."""
from modules.library.intake import find_isbns, is_valid_isbn13, normalise_isbn
NOTE = """
Northwind Books -- delivery note 2026-09
Dune 978-0-441-01359-3 3 copies
A Brief History of Time 9780553380163 1 copy
Order reference 2026 0904 1122
"""
def test_it_finds_the_isbns_however_they_are_printed() -> None:
found = [line.isbn for line in find_isbns(NOTE)]
assert found == ["9780441013593", "9780553380163"]
def test_an_order_reference_is_not_an_isbn() -> None:
"""`2026 0904 1122` is thirteen digits. A looser pattern would order a book."""
assert find_isbns("Order reference 2026 0904 1122") == ()
def test_a_misread_digit_is_dropped_not_reported() -> None:
"""OCR turns 3 into 8. The checksum catches it; surfacing it invites a click."""
assert is_valid_isbn13("9780441013593")
assert not is_valid_isbn13("9780441013598")
assert find_isbns("978-0-441-01359-8") == ()

Notice what is tested: not only “it finds the ISBNs”, but what it refuses. A thirteen-digit order reference and a misread digit are the two ways this function can order the wrong book. Those are the ones worth a test.

Integration: against a real database

@pytest.fixture
def engine(tmp_path: Path) -> Engine:
"""A database with this module's tables in it, and nothing else running."""
engine = create_engine(f"sqlite:///{tmp_path / 'library.db'}")
build_metadata(_frozen()).create_all(engine)
return engine
def test_the_tables_exist_and_a_loan_can_be_written(engine: Engine) -> None:
metadata = build_metadata(_frozen())
loans = metadata.tables["library_loan"]
...
with engine.connect() as conn:
assert conn.execute(select(loans.c.state)).scalar_one() == "out"

And the state machine, which is the module’s contract:

def test_a_returned_loan_cannot_be_returned_again(self) -> None:
"""Nothing in the module has to check this: the machine has no such edge."""
machine = get_state_machine("library_loan").machine
assert machine.transition_for("check_in", current_state="returned") is None
with pytest.raises(InvalidTransitionError):
machine.fire("check_in", current_state="returned")

Running them

Fenêtre de terminal
# Yours only, while you are writing.
pytest modules/library/tests -q
# Everything, the way CI does.
pytest kewos/tests modules -q
.......... [100%]
10 passed in 1.23s

What people forget to write

For every nominal case, the project asks for two more, and it is not a formality:

  • an edge case: the empty value, the one-element list, today’s date;
  • an expected error: permission refused, validation failing, the forbidden transition. A test that only checks success says nothing about what happens when things go wrong, and that is where users live;
  • concurrency, if the feature touches a transaction, a lock or idempotence.

One last piece of advice, learned writing the other pages of this site: a test that inspects nothing passes for the wrong reason. If your test walks a list of files or of models, assert first that it found some, or it will stay green the day discovery breaks.