Les tests
Les tests d’un module vivent dans le module. Pas dans un dossier global, pas à côté du
noyau : dans modules/<votre module>/tests/, avec le code qu’ils décrivent.
Rien à configurer : modules fait déjà partie des chemins que pytest parcourt.
Le détail qui n’est pas facultatif
modules/library/ tests/ __init__.py ← celui-ci test_intake.py test_loans.pySans ce fichier vide, pytest importe vos tests sous leur nom nu. Le premier module s’en
tire ; le deuxième qui nomme un fichier test_models.py fait échouer la collecte de
toute la suite, pas seulement de la sienne :
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.pyLa panne tombe sur celui qui arrive en second, loin de sa cause. Un test du noyau vérifie
maintenant que chaque dossier tests/ d’un module porte son __init__.py.
Trois niveaux, trois questions différentes
| À quoi ça répond | Ce que ça touche | |
|---|---|---|
| Unitaire | ma logique est-elle juste ? | rien : pas de base, pas de réseau, pas de file |
| Intégration | mon module tient-il face à ses dépendances réelles ? | une base, le registre, le moteur d’états |
| Bout en bout | le parcours de l’utilisateur marche-t-il ? | HTTP → service → ORM → base |
Les trois ne se remplacent pas. Un test unitaire vert sur une fonction que personne n’appelle ne prouve rien ; un test d’intégration ne dit pas pourquoi un calcul est faux.
Unitaire : la logique, seule
Le module de bibliothèque lit un bon de livraison et en extrait des ISBN. C’est du texte qui entre, des ISBN qui sortent : ça se teste avec du texte.
"""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") == ()Remarquez ce qui est testé : pas seulement « il trouve les ISBN », mais ce qu’il refuse. Une référence de commande à treize chiffres et un chiffre mal lu sont les deux façons dont cette fonction peut commander le mauvais livre. Ce sont elles qui méritent un test.
Intégration : contre une vraie base
@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"Et la machine à états, qui est le contrat du module :
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")Les lancer
# Ceux de votre module seulement, pendant que vous écrivez.pytest modules/library/tests -q
# Tout, comme l'intégration continue.pytest kewos/tests modules -q.......... [100%]10 passed in 1.23sCe qu’on oublie d’écrire
Pour chaque cas nominal, le projet en demande deux autres, et ce n’est pas une formalité :
- un cas limite : la valeur vide, la liste à un élément, la date d’aujourd’hui ;
- une erreur attendue : le droit refusé, la validation qui échoue, la transition interdite. Un test qui ne vérifie que le succès ne dit rien de ce qui arrive quand ça se passe mal, et c’est là que les utilisateurs vivent ;
- la concurrence, si la fonctionnalité touche à une transaction, un verrou ou une idempotence.
Un dernier conseil, appris en écrivant les autres pages de ce site : un test qui n’inspecte rien passe pour une mauvaise raison. Si votre test parcourt une liste de fichiers ou de modèles, vérifiez d’abord qu’il en a trouvé, sinon il restera vert le jour où la découverte casse.