Introduction à ArchUnit

1. Vue d'ensemble

Dans cet article, nous montrerons comment vérifier l'architecture d'un système à l'aide d' ArchUnit .

2. Qu'est-ce que ArchUnit?

Le lien entre les caractéristiques de l'architecture et la maintenabilité est un sujet bien étudié dans l'industrie du logiciel. Cependant, définir une architecture saine pour nos systèmes ne suffit pas. Nous devons vérifier que le code implémenté y adhère.

En termes simples, ArchUnit est une bibliothèque de test qui nous permet de vérifier qu'une application adhère à un ensemble donné de règles architecturales . Mais qu'est-ce qu'une règle architecturale? Plus encore, qu'entend-on par architecture dans ce contexte?

Commençons par ce dernier. Ici, nous utilisons le terme architecture pour désigner la façon dont nous organisons les différentes classes de notre application en packages .

L'architecture d'un système définit également comment les packages ou groupes de packages - également appelés couches - interagissent. En termes plus pratiques, il définit si le code d'un package donné peut appeler une méthode dans une classe appartenant à une autre. Par exemple, supposons que l'architecture de notre application contienne trois couches: présentation , service et persistance .

Une façon de visualiser l'interaction de ces couches consiste à utiliser un diagramme de package UML avec un package représentant chaque couche:

En regardant simplement ce diagramme, nous pouvons déterminer quelques règles:

  • Les classes de présentation ne doivent dépendre que des classes de service
  • Les classes de service ne doivent dépendre que des classes de persistance
  • Les classes de persistance ne devraient dépendre de personne d'autre

En regardant ces règles, nous pouvons maintenant revenir en arrière et répondre à notre question initiale. Dans ce contexte, une règle architecturale est une assertion sur la manière dont nos classes d'application interagissent les unes avec les autres.

Alors maintenant, comment vérifier que notre implémentation respecte ces règles? C'est là qu'intervient ArchUnit . Il nous permet d'exprimer nos contraintes architecturales à l'aide d'une API fluide et de les valider avec d'autres tests lors d'une construction régulière.

3. Configuration du projet ArchUnit

ArchUnit s'intègre parfaitement au framework de test JUnit , et donc, ils sont généralement utilisés ensemble. Tout ce que nous avons à faire est d'ajouter la dépendance archunit-junit4 pour correspondre à notre version JUnit :

 com.tngtech.archunit archunit-junit4 0.14.1 test  

Comme son artifactId l' implique, cette dépendance est spécifique au framework JUnit 4.

Il existe également une dépendance archunit-junit5 si nous utilisons JUnit 5:

 com.tngtech.archunit archunit-junit5 0.14.1 test 

4. Ecriture de tests ArchUnit

Une fois que nous avons ajouté la dépendance appropriée à notre projet, commençons à écrire nos tests d'architecture. Notre application de test sera une simple application SpringBoot REST qui interroge les Schtroumpfs. Pour plus de simplicité, cette application de test contient uniquement les classes Controller , Service et Repository .

Nous voulons vérifier que cette application est conforme aux règles que nous avons mentionnées précédemment. Commençons donc par un simple test de la règle «les classes de présentation ne doivent dépendre que des classes de service».

4.1. Notre premier test

La première étape consiste à créer un ensemble de classes Java qui seront vérifiées pour les violations de règles . Nous faisons cela en instanciant la classe ClassFileImporter puis en utilisant l'une de ses méthodes importXXX () :

JavaClasses jc = new ClassFileImporter() .importPackages("com.baeldung.archunit.smurfs");

Dans ce cas, l' instance JavaClasses contient toutes les classes de notre package d'application principal et de ses sous-packages. Nous pouvons considérer cet objet comme étant analogue à un sujet de test typique utilisé dans les tests unitaires réguliers, car il sera la cible des évaluations de règles.

Les règles architecturales utilisent l'une des méthodes statiques de la classe ArchRuleDefinition comme point de départ pour ses appels d' API fluides . Essayons d'implémenter la première règle définie ci-dessus en utilisant cette API. Nous utiliserons la méthode classes () comme point d'ancrage et ajouterons des contraintes supplémentaires à partir de là:

ArchRule r1 = classes() .that().resideInAPackage("..presentation..") .should().onlyDependOnClassesThat() .resideInAPackage("..service.."); r1.check(jc);

Notez que nous devons appeler la méthode check () de la règle que nous avons créée pour exécuter la vérification. Cette méthode prend un objet JavaClasses et lèvera une exception en cas de violation.

Tout cela semble bon, mais nous obtiendrons une liste d'erreurs si nous essayons de l'exécuter avec notre code:

java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'classes that reside in a package '..presentation..' should only depend on classes that reside in a package '..service..'' was violated (6 times): ... error list omitted 

Pourquoi? Le principal problème avec cette règle est le onlyDependsOnClassesThat () . Malgré ce que nous avons mis dans le diagramme de package, notre implémentation réelle a des dépendances sur les classes de framework JVM et Spring, d'où l'erreur.

4.2. Réécrire notre premier test

Une façon de résoudre cette erreur consiste à ajouter une clause qui prend en compte ces dépendances supplémentaires:

ArchRule r1 = classes() .that().resideInAPackage("..presentation..") .should().onlyDependOnClassesThat() .resideInAPackage("..service..", "java..", "javax..", "org.springframework.."); 

Avec ce changement, notre vérification cessera d'échouer. Cette approche, cependant, souffre de problèmes de maintenabilité et semble un peu piratée. Nous pouvons éviter ces problèmes en réécrivant notre règle en utilisant la méthode statique noClasses () comme point de départ:

ArchRule r1 = noClasses() .that().resideInAPackage("..presentation..") .should().dependOnClassesThat() .resideInAPackage("..persistence.."); 

Bien sûr, nous pouvons également souligner que cette approche est basée sur le refus au lieu de celle basée sur l' autorisation que nous avions auparavant. Le point critique est que quelle que soit l'approche que nous choisissons, ArchUnit sera généralement suffisamment flexible pour exprimer nos règles .

5. Utilisation de l' API Library

ArchUnit facilite la création de règles architecturales complexes grâce à ses règles intégrées. Ceux-ci, à leur tour, peuvent également être combinés, ce qui nous permet de créer des règles en utilisant un niveau d'abstraction plus élevé. Prêt à l' emploi , ArchUnit propose l' API Library , un ensemble de règles pré-packagées qui répondent aux problèmes d'architecture courants :

  • Architectures: Support for layered and onion (a.k.a. Hexagonal or “ports and adapters”) architectures rule checks
  • Slices: Used to detect circular dependencies, or “cycles”
  • General: Collection of rules related to best coding practices such as logging, use of exceptions, etc.
  • PlantUML: Checks whether our code base adheres to a given UML model
  • Freeze Arch Rules: Save violations for later use, allowing to report only new ones. Particularly useful to manage technical debts

Covering all those rules is out of scope for this introduction, but let's take a look at the Architecture rule package. In particular, let's rewrite the rules in the previous section using the layered architecture rules. Using these rules requires two steps: first, we define the layers of our application. Then, we define which layer accesses are allowed:

LayeredArchitecture arch = layeredArchitecture() // Define layers .layer("Presentation").definedBy("..presentation..") .layer("Service").definedBy("..service..") .layer("Persistence").definedBy("..persistence..") // Add constraints .whereLayer("Presentation").mayNotBeAccessedByAnyLayer() .whereLayer("Service").mayOnlyBeAccessedByLayers("Presentation") .whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service"); arch.check(jc);

Here, layeredArchitecture() is a static method from the Architectures class. When invoked, it returns a new LayeredArchitecture object, which we then use to define names layers and assertions regarding their dependencies. This object implements the ArchRule interface so that we can use it just like any other rule.

La chose intéressante à propos de cette API particulière est qu'elle nous permet de créer en seulement quelques lignes de règles de code qui nous obligeraient autrement à combiner plusieurs règles individuelles.

6. Conclusion

Dans cet article, nous avons exploré les bases de l'utilisation d' ArchUnit dans nos projets. L'adoption de cet outil est une tâche relativement simple qui peut avoir un impact positif sur la qualité globale et réduire les coûts de maintenance à long terme.

Comme d'habitude, tout le code est disponible sur sur GitHub.