ArchUnit : comment l’utiliser pour contrôler l’architecture de vos projets Java ?

Vous êtes développeur et vous vous lancez dans la conception d’un nouveau projet ? L’une des premières étapes cruciales consiste à déterminer l’architecture logicielle qui sera utilisée. « Les classes seront-elles regroupées par couche ou par fonctionnalité ? ». « Est-il préférable d’avoir une architecture MVC, MVP, hexagonale, etc. ? ». « Devons-nous envisager un découpage par module ? ». Ce sont autant de questions auxquelles il vous faudra répondre avant même d’écrire votre première ligne de code.

Cependant, la vraie difficulté commence une fois l’architecture définie. Le plus dur reste désormais de s’assurer que l’ensemble des développements respecte les règles que vous vous êtes fixées. Je ne compte plus le nombre de fois où j’ai pu déceler des soucis architecturaux lors de merge requests. « Cette classe devrait plutôt être dans tel package », « cette classe ne devrait pas utiliser ces modèles ».

Cette situation vous semble familière ? Et si je vous disais qu’il existe un outil qui peut faire ces contrôles à votre place ? ArchUnit est une librairie Java qui va vous permettre de vérifier automatiquement que votre code respecte vos règles d’architecture. Nul besoin d’outil externe, le tout est validé au travers de vos tests unitaires ! Dites au revoir aux contrôles manuels, faillibles et chronophages. Dans cet article, je vais vous présenter comment utiliser ArchUnit au travers de cas d’usage concrets, sélectionnés à partir de ce que j’ai pu mettre en place sur mes précédents projets.

Bien qu’il soit préférable d’utiliser ArchUnit aux prémices de votre projet, vous constaterez qu’il est tout à fait possible de l’intégrer à un projet déjà bien avancé. Afin que vous puissiez avoir des exemples concrets auxquels vous référer, cet article contient de nombreux extraits de code. Pour simplifier leur lecture, certains d’entre eux ont été abrégés, mais l’exhaustivité des sources présentées est disponible dans le dépôt GitHub archunit-sample.

Contrôler automatiquement l’intégrité architecturale de votre application Java à l’aide d’ArchUnit

Imaginez ne plus jamais perdre de temps à vérifier manuellement que votre code respecte les règles d’architecture que vous avez définies. Avec ArchUnit, vous pouvez vous concentrer sur ce qui compte vraiment pour vous : produire du code de qualité et développer de nouvelles fonctionnalités innovantes pour vos clients.

ArchUnit vous permet de définir vos propres règles architecturales, qui seront automatiquement vérifiées lors de l’exécution de vos tests unitaires. Si une violation est détectée, le test vous en informe immédiatement et vous explique comment y remédier. Vous gagnez ainsi en productivité tout en assurant la cohérence et la maintenabilité de votre projet.

Vous vous demandez sans doute comment est-ce que ça fonctionne et surtout, est-ce que c’est simple à mettre en place et utiliser ? Regardons ce qui se cache sous le capot et évoquons ensemble la maintenabilité de ces tests architecturaux.

ArchUnit est une librairie conçue pour vérifier automatiquement l’architecture logicielle de votre application au travers de n’importe quel framework de test Java (typiquement, JUnit).

Techniquement, ArchUnit repose sur l’analyse du bytecode Java, et sa matérialisation sous forme de classes spécifiques à ArchUnit. Par exemple, les classes JavaClass et JavaMethod représentent respectivement les classes et les méthodes de votre projet.

Ce sont ces structures de données qui vont vous permettre de mettre en place vos tests d’architecture. Une librairie à importer et une nouvelle classe à créer, c’est tout ce qu’il vous faut pour commencer à tester automatiquement l’architecture de votre projet.

Simple à intégrer et extensible à souhait, ArchUnit est la solution idéale pour détecter automatiquement les écarts architecturaux de votre projet.

Intégration d’ArchUnit, premiers pas vers l’automatisation des tests architecturaux de votre application

Au sein d’un projet qui utilise déjà JUnit, l’intégration d’ArchUnit ne nécessite que l’ajout de la librairie archunit-junit5 (ou archunit-junit4, si vous utilisez toujours JUnit 4).
Exemple d’intégration via Gradle :

Maintenant qu’ArchUnit est intégré à votre projet, il est temps de créer votre premier test d’architecture.

ArchitectureTest.java – Github.com1

 

Décortiquons ensemble les informations présentes dans l’extrait de code ci-dessus.

  • #1 – L’annotation @AnalyzeClasses permet d’indiquer les classes qui seront testées par ArchUnit.
  • #2 – L’annotation @ArchTest (en remplacement de l’annotation @Test) rend possible l’injection des JavaClass au sein de vos méthodes de test.
  • #3 – La méthode .that() permet de ne conserver que les classes qui correspondent aux conditions qui suivent.
  • #4 – La méthode .should() applique les règles qui suivent aux classes qui ont été conservées.

Le test ci-dessus permet donc de :

  • #1 – Charger l’ensemble des classes présentes dans le package fr.arthurrousseau.archunit
  • #3 – Filtrer et ne conserver que les classes qui sont présentes dans le package *.service.impl
  • #4 – Vérifier que l’ensemble des classes qui correspondent à ces filtres sont annotées avec l’annotation @Service.

Pour tester cette règle, admettons qu’au sein de votre projet vous disposez d’une classe ProductServiceImpl n’étant pas annotée @Service :

Voici le résultat que retournerait l’exécution des tests d’architecture :

La classe ProductServiceImpl ne respectant pas la règle décrite, le test échoue. Les informations tracées lors de l’exécution des tests ArchUnit permettent de cibler l’origine du problème.

Aussi simple soit-elle, cette première règle vous rapproche un peu plus de l’automatisation du contrôle de l’architecture de votre projet !

Définir l’architecture cible de votre application à l’aide de règles unitaires et composites

Maintenant que vous avez écrit votre première règle ArchUnit, passons à son intégration dans un projet plus complet. A partir de maintenant, les extraits de code qui suivent sont issus du projet archunit-sample.

Le package controller contient les classes qui exposent les points d’entrée permettant d’effectuer des opérations sur les produits que gère le projet.

Afin de s’assurer de la cohérence de nos points d’entrée, il a été décidé que l’ensemble des controllers soient annotés avec l’annotation @Controller, possèdent le suffixe Controller et soient positionnés à la racine du package « ..controller ».

Cette cohérence peut être assurée à l’aide des trois règles unitaires suivantes :

Mais elles peuvent également être regroupées au sein d’une seule et même règle composite :

ControllerTest.java – Github.com2

 

En regroupant ces vérifications au sein d’une seule et même règle, vous centralisez vos contrôles, vous rendez plus naturelle la compréhension de vos règles et vous simplifiez leur maintenance (si demain vous décidiez de modifier votre règle pour utiliser des @RestController plutôt que des @Controller, vous n’auriez qu’une seule règle à modifier).

Définir des contraintes d’architecture complexes à l’aide de règles personnalisées

Toujours pour assurer la cohérence des points d’entrée, voyons comment faire pour s’assurer que ceux-ci ne manipulent que des objets qui leur sont dédiés. Interdiction donc de recevoir ou de renvoyer des objets du package domain : les objets utilisés devront provenir du package controller.model

Jusqu’à présent, vous avez utilisé quelques-unes des 1633 méthodes que met à disposition ArchUnit. Ces méthodes telles que beAnnotatedWith et haveSimpleNameEndingWith sont particulièrement pratiques, mais vous aurez parfois besoin d’aller plus loin.

Pour mettre en place cette nouvelle règle vous allez devoir utiliser une condition personnalisée. Ce type de condition va vous permettre de mettre en place des règles plus poussées avec une extrême simplicité. En effet, il vous suffit d’étendre la classe ArchCondition et implémenter la méthode check pour lui faire faire ce que vous souhaitez !

Voici la signature de la méthode que vous devrez implémenter :

Le premier paramètre, clazz, correspond à la classe qui est en train d’être contrôlée. Le second paramètre, events, fait office de registre de violations de règles. A chaque fois qu’une violation sera constatée sur la classe en cours de test, c’est au travers de cet objet qu’elle devra être tracée.

ControllerTest.java – Github.com4

 

L’exemple ci-dessus présente une façon d’atteindre nos objectifs. Les éléments les plus importants de cette implémentation sont les suivants :

  • #1 – Message associé à la condition, utilisé lorsque pour créer les traces d’erreur en cas de violation,
  • #2 – On vérifie, au travers des objets fournis par ArchUnit, que l’objet retourné se trouve bien dans le package controller.model et possède un nom qui termine par Dto,
  • #3 – events.add(SimpleConditionEvent.**violated**(method, message)); méthode permettant de tracer le fait que la méthode testée n’a pas respecté la condition personnalisée.

Pour utiliser cette condition, il suffit de l’associer à une nouvelle règle :

Le controller ProductsController possède une méthode add() qui prend en entrée un objet de type Product qui réside dans le package domain.model.

La règle que vous venez de mettre en place lève une erreur et indique, comme vous pouviez vous y attendre, que ce controller manipule des données qui ne sont pas propres au package controller.model :

Identifier et rectifier les problèmes d’architecture au fil de l’eau grâce au gel des règles ArchUnit

Lors de l’ajout de nouvelles règles au sein de projets existants, il est possible qu’un certain nombre de violations existantes soient détectées.
Parfois, leur nombre est tel qu’il n’est pas possible d’y remédier immédiatement. La meilleure façon de traiter ces violations consiste à les traiter petit à petit, de façon itérative.

Les règles d’architecture peuvent être gelées à l’aide de la classe FreezingArchRule. Le fait de geler une règle enregistre l’ensemble des violations actuelles dans un ViolationStore. De cette façon, lors des prochaines exécutions, seules les nouvelles violations lèveront une erreur. Les violations listées lors du gel de la règle seront supprimées du ViolationStore dès leur correction.

Pour geler une règle, il suffit de l’encapsuler dans la méthode FreezingArchRule.freeze(rule)) :

ControllerTest.java – Github.com5

 

En plus de cela, il vous sera nécessaire d’autoriser la création d’un nouveau store en créant un fichier archunit.properties au sein des ressources de votre projet :

archunit.properties – Github.com6

 

Suite à la première exécution de cette règle gelée, vous constaterez qu’ArchUnit a créé deux nouveaux fichiers dans le dossier archunit_store :

stored.rules – Github.com7

archunit-sample-project8

 

Le premier fichier contient la liste des règles gelées et leurs identifiants. Le second fichier contient quant à lui l’ensemble des violations existantes au moment du gel de la règle. Chaque règle gelée possède son propre fichier, nommé en fonction d’un identifiant unique généré par ArchUnit.

Contrôler l’architecture globale de votre application

Si vous souhaitez vous assurer que l’architecture globale de votre projet est respectée, alors vous aurez besoin d’utiliser des fonctions du package com.tngtech.archunit.library.

Ce package comporte une large collection de règles prédéfinies qui seraient complexes à mettre en place au travers de simples règles.

Dans le cadre du projet archunit-sample, voici les règles qui composent l’architecture globale du projet :

  • Le package controller n’est accédé par aucun autre package. Ce package donne accès au package service.
  • Le package service est indépendant, il ne dépend ni du package controller, ni du package repository9.
  • Le package repository n’est accédé par aucun autre package*. Ce package a accès au package service.

Cette architecture peut être vérifiée à l’aide de la règle ci-dessous :

ControllerTest.java – Github.com10

 

Au travers d’une syntaxe simple, chacun des packages du projet est associé à une couche de l’architecture de l’application. Chacune des couches déclarées peut alors spécifier à quelle autre couche elle a accès, mais aussi quelle autre couche a droit d’y accéder.

Garder l’architecture de son projet sous contrôle et gagner du temps à l’aide d’ArchUnit

Mettre en place l’architecture logicielle d’un projet est une chose, s’assurer qu’elle soit respectée et maintenue en est une autre.

ArchUnit permet de définir et de vérifier automatiquement que votre projet respecte les règles architecturales que vous vous êtes fixées. En vous affranchissant de ces vérifications manuelles, vous aurez plus de temps pour vous concentrer sur ce qui compte vraiment pour vous : produire du code de qualité et développer de nouvelles fonctionnalités innovantes pour vos clients.

L’intégration d’ArchUnit dans vos projets Java est simple et rapide : il vous suffit d’ajouter la librairie qui correspond à votre version de JUnit et le tour est joué ! Sa flexibilité et son extensibilité en font un outil puissant capable de s’adapter aux besoins spécifiques de chaque projet. De plus, la possibilité de geler des règles vous permet de remédier progressivement aux violations existantes, sans impact immédiat sur vos développements.

Vous êtes en quête d’idées de règles à appliquer à vos projets ? N’hésitez pas à consulter la documentation officielle d’ArchUnit. Vous y trouverez de nombreux cas d’usage qui pourront vous aider à trouver l’inspiration.

Pour les utilisateurs avancés, voici quelques pistes que vous pourriez explorer pour aller plus loin.

  • Mettre en place une règle pour vérifier que vous n’avez pas d’annotations @Autowired sur vos attributs (préférez les injections par constructeur),
  • Exporter vos règles dans une librairie dédiée (idéal si vous avez de nombreux projets qui doivent respecter les mêmes règles),
  • Générer vos règles ArchUnit à partir de diagrammes de classe PlantUML (exemple à cette adresse)

Si à la suite de cet article vous avez sauté le pas, n’hésitez pas à partager vos retours d’expérience. Je suis curieux de savoir comment vous avez adopté ArchUnit au sein de vos applications !

1 Arthur Rousseau, « archunit-sample-project », github.com, Architecture test Java, 2024.

2 Arthur Rousseau, « archunit-sample-project », github.com, Controller, test Java, 2024.

3 Nombre de méthodes disponibles en date 25 juin 2024.

4 Arthur Rousseau, « Archunit-sample-project », github.com, Controller, test Java, 2024

5 Arthur Rousseau, « archunit-sample-project », github.com, Controller, test Java, 2024.

6 Arthur Rousseau, « archunit.properties », github.com, archunit.properties, , test Java, 2024.

7 Arthur Rousseau, « archunit-sample-project », github.com, stored.rules, 2024.

8 Ibid.

9 Techniquement, le package service est indépendant des autres puisqu’il expose une interface ProductRepository qui est implémentée par le package repository.

10 Arthur Rousseau, « archunit-sample-project », github.com, ArchitectureTest.java, 2024.

Auteur

  • Arthur Rousseau

    Associate Architecture