Tests d'architecture
Toutes les règles posées dans cette section ont un point commun : ce sont des intentions. « Le domaine ne dépend de rien », « une couche n’appelle que celle du dessous », « seule la façade du module est importable », « aucun ORM dans le domaine ». Rien, dans un dépôt ordinaire, ne les empêche d’être violées — et une règle d’architecture non vérifiée est violée en quelques semaines, sans mauvaise volonté de personne.
Cette page est le garde-fou promis plusieurs fois ailleurs :
Une frontière qui n’échoue pas en intégration continue n’est pas une frontière, c’est une convention.
Le terme générique est fonction d’aptitude (fitness function), emprunté à Building Evolutionary Architectures : une caractéristique architecturale rendue exécutable. Un test d’architecture en est le cas particulier le plus courant et le plus rentable.
Ce qui est vraiment testable
Toutes les propriétés d’une architecture ne le sont pas. Voici celles qui le sont, par ordre de rentabilité.
| Ce qu’on vérifie | Exemple de règle | |
|---|---|---|
| 1 | Dépendances | domaine n’importe ni infrastructure, ni aucune bibliothèque tierce |
| 2 | Cycles | Aucun cycle entre modules |
| 3 | Surface publique | expedition n’atteint facturation que par sa façade |
| 4 | Emplacement et nommage | Un cas d’usage vit dans application/ ; un contrôleur finit par Controller |
| 5 | Absence de dépendance technique | Le domaine n’importe pas sqlalchemy, typeorm, sqlx |
| 6 | Contrats | Le schéma d’événement reste compatible avec la version précédente |
| 7 | Caractéristiques mesurables | Temps de démarrage, taille du paquet, nombre de requêtes SQL par cas d’usage |
Les cinq premières sont statiques, se vérifient en quelques secondes et couvrent la
quasi-totalité des règles de cette section. La sixième relève des tests de contrat, la
septième est la plus négligée — et souvent la plus intéressante, parce qu’elle attrape
des régressions qu’aucune revue ne voit, comme un N+1 apparu dans un cas d’usage.
Les outils
| Langage | Outil | Nature de la garantie |
|---|---|---|
| Rust | Le système de crates lui-même | Structurelle : le code interdit ne compile pas |
| Java | ArchUnit | Tests unitaires ordinaires |
| .NET | NetArchTest | Tests unitaires ordinaires |
| Python | import-linter | Contrats déclaratifs, une commande |
| TypeScript | dependency-cruiser, eslint-plugin-boundaries | Règles déclaratives, une commande |
En pratique
En Rust, l’essentiel du travail est fait par le langage : un mod privé est
inatteignable, et une dépendance non déclarée ne compile pas. cargo-deny ne sert
qu’à empêcher qu’une dépendance interdite arrive transitivement dans la crate du
domaine.
Le nommage et l’emplacement
La quatrième catégorie mérite un mot, parce qu’elle est souvent jugée cosmétique alors qu’elle protège une convention réelle.
Vérifier qu’un cas d’usage vit dans application/ n’est pas une question de style :
c’est ce qui empêche qu’un cas d’usage apparaisse dans infrastructure/, où la règle
de dépendance ne le protégerait plus. De même, une classe qui finit par Repository
et qui n’implémente pas le port correspondant signale une confusion entre dépôt et
DAO — distinction traitée dans Services, dépôts et
fabriques.
Ces règles ont un défaut symétrique : trop nombreuses, elles deviennent du bruit qu’on désactive en bloc. Trois ou quatre suffisent.
Où les faire tourner
Comme des tests, avec les tests. Un échec de règle d’architecture doit casser la construction exactement comme un test unitaire — pas produire un avertissement que personne ne lit.
Le placement dans le pipeline importe : ces vérifications sont rapides — quelques secondes — et doivent donc passer avant la suite de tests longue. Un développeur qui découvre une violation de frontière après huit minutes de tests d’intégration la contournera.
Introduire dans un code existant
C’est le point où la plupart des tentatives échouent, et il a une solution connue.
Activer toutes les règles d’un coup sur un code existant produit trois cents échecs et une seule réaction possible : désactiver. La technique qui fonctionne est le cliquet — baseline and ratchet :
- Mesurer l’existant : combien de violations aujourd’hui, et lesquelles.
- Enregistrer ce nombre comme référence tolérée.
- Faire échouer la CI dès que le nombre augmente. Les violations existantes passent, les nouvelles non.
- Abaisser la référence à chaque fois qu’une violation est corrigée. Le cliquet ne remonte jamais.
L’effet est immédiat sur ce qui compte : le code nouveau respecte la règle dès le premier jour, et l’ancien s’améliore au rythme des passages. C’est la même logique que l’étranglement progressif appliquée à une contrainte plutôt qu’à du code.
Tip
Une variante plus simple et souvent suffisante quand l’outil ne gère pas de référence : n’appliquer les règles qu’à un périmètre restreint — le module de cœur de métier — et l’étendre module par module. Une règle appliquée à 10 % du code et respectée vaut mieux qu’une règle appliquée partout et désactivée.
Les limites
Ils vérifient la structure, pas la conception. C’est la limite fondamentale : un modèle anémique passe toutes les règles de dépendance du monde. Des entités sans comportement, des services qui font tout le travail, une couche traversante — rien de tout cela n’est détectable par un test d’architecture. Ils garantissent que les règles ne dépendent de rien ; pas qu’il y ait des règles.
Ils ne disent pas si la frontière est au bon endroit. Un découpage médiocre, rigoureusement respecté, reste un découpage médiocre.
Ils créent un risque de fausse confiance. Une CI verte sur les règles d’architecture peut faire croire que la conception va bien. C’est le complément d’une revue, pas son remplacement.
Ils ont un coût de maintenance. Un remaniement légitime fait échouer des règles écrites pour une structure antérieure, et il faut alors décider — modifier la règle ou renoncer au remaniement. C’est un arbitrage sain, mais c’est du temps.
Ce qu’il faut mettre en place, au minimum
Si une seule chose devait être faite, dans cet ordre :
- Une règle de dépendance sur le domaine : il n’importe ni l’infrastructure, ni aucune bibliothèque technique. C’est celle qui protège la décision la plus coûteuse à défaire.
- Une règle anti-cycles entre modules. Gratuite à écrire, et elle attrape des dégradations que personne ne voit venir.
- Une règle de façade si le code est organisé en modules — voir Monolithe, modulaire, microservices.
Trois règles, quelques secondes de CI, et l’essentiel de ce que cette section recommande cesse d’être une intention.
Références
- Neal Ford, Rebecca Parsons & Patrick Kua, Building Evolutionary Architectures, 2ᵉ édition, 2022 — l’origine des fonctions d’aptitude
- https://www.archunit.org/ — la référence, dont le vocabulaire est utile même hors JVM
- https://import-linter.readthedocs.io/
- https://github.com/sverweij/dependency-cruiser
- https://embarkstudios.github.io/cargo-deny/
- https://docs.pact.io/ — tests de contrat pilotés par le consommateur