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.pyWithout 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.pywhich is not the same as the test file we want to collect: modules/library/tests/test_models.pyThe 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 answers | What it touches | |
|---|---|---|
| Unit | is my logic right? | nothing: no database, no network, no queue |
| Integration | does my module hold against its real dependencies? | a database, the registry, the state machine |
| End to end | does 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-09Dune 978-0-441-01359-3 3 copiesA Brief History of Time 9780553380163 1 copyOrder 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.fixturedef 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
# 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.23sWhat 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.