Tests d’Architecture avec ArchUnit
Les tests d’architecture garantissent automatiquement le respect de l’architecture hexagonale.
Sans tests automatisés, rien n’empêche un développeur de violer l’architecture. ArchUnit est la solution !
Pourquoi des Tests d’Architecture ?
❌ Problème
Le Problème Sans Tests
Sans tests automatisés, rien n’empêche un développeur de faire ça :
// ❌ VIOLATION : Le domain dépend de l'infrastructure
package org.smoka.domain.model;
import org.smoka.infrastructure.adapter.TrendRepositoryAdapter; // ❌ INTERDIT !
public class TrendResult {
private TrendRepositoryAdapter adapter; // ❌ Le domain ne doit pas connaître les adapters
}Ou ça :
// ❌ VIOLATION : L'application dépend de l'infrastructure
package org.smoka.application.usecase;
import org.smoka.infrastructure.adapter.TrendRepositoryAdapter; // ❌ INTERDIT !
public class GetTrendsUseCase {
private TrendRepositoryAdapter adapter; // ❌ Doit dépendre de l'interface TrendRepository
}Conséquences :
- ❌ Architecture compromise
- ❌ Couplage fort
- ❌ Impossible de changer d’implémentation
- ❌ Tests difficiles
Installation
Ajouter la dépendance Maven
Ajoutez ArchUnit dans votre pom.xml :
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>1.3.0</version>
<scope>test</scope>
</dependency>Créer le fichier de test
Créez le fichier suivant :
Configurer le setup
@BeforeAll
static void setup() {
// Importe toutes les classes du projet
classes = new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.withImportOption(location -> !location.contains("$")) // Exclure classes générées
.importPackages("org.smoka");
}Important :
DO_NOT_INCLUDE_TESTS: Exclut les classes de test!location.contains("$"): Exclut les classes générées par Micronaut, Lombok et le compilateur
Règles d’Architecture
1. Règles de Dépendances entre Couches
Domain Indépendant
Le DOMAIN ne dépend de RIEN
@Test
void domainShouldNotDependOnAnything() {
noClasses()
.that().resideInAPackage("..domain..")
.should().dependOnClassesThat().resideInAnyPackage(
"..application..",
"..infrastructure.."
)
.check(classes);
}Vérifie que :
domain/model/TrendQuery.javane dépend PAS deapplication/domain/model/TrendResult.javane dépend PAS deinfrastructure/domain/port/output/TrendRepository.javane dépend PAS de frameworks
Autorisé :
- ✅
java.*(Collections, LocalDateTime, Optional, etc.) - ✅
lombok.*(annotations pour réduire le boilerplate) - ✅ Autres classes du
domain/
Interdit :
- ❌
io.micronaut.* - ❌
jakarta.persistence.* - ❌
application.* - ❌
infrastructure.*
2. Règles des Ports de Sortie
Ports = Interfaces
Les Ports de Sortie sont des INTERFACES
@Test
void outputPortsShouldBeInterfacesInDomain() {
classes()
.that().resideInAPackage("..domain.port.output..")
.should().beInterfaces()
.check(classes);
}Vérifie que :
TrendRepository.javaest bien une interface ✅- Pas de classes concrètes dans
domain/port/output/❌
Les ports de sortie sont des contrats que l’infrastructure doit respecter.
3. Règles des Adaptateurs
Adapters → Ports
Les Adaptateurs Implémentent des Ports
@Test
void adaptersShouldImplementDomainPorts() {
classes()
.that().resideInAPackage("..infrastructure.adapter..")
.and().haveSimpleNameEndingWith("Adapter")
.should().dependOnClassesThat().resideInAPackage("..domain.port.output..")
.check(classes);
}Vérifie que :
MockTrendRepositoryAdapterdépend deTrendRepository(interface du domain) ✅- Si un adapter n’implémente aucun port, le test échouera ❌
On utilise dependOnClassesThat() au lieu de implement() car c’est plus flexible et fonctionne avec l’API ArchUnit.
4. Règles des Use Cases
Package Correct
Les Use Cases sont dans le Bon Package
@Test
void useCasesShouldResideInCorrectPackage() {
classes()
.that().haveSimpleNameEndingWith("UseCase")
.should().resideInAPackage("..application.usecase..")
.check(classes);
}Convention de nommage :
- ✅
GetTrendsUseCasedansapplication.usecase/ - ❌
TrendServicedansapplication.service/
5. Règles du Domain
Domain Pur
Le Domain est PUR (pas d’annotations framework)
@Test
void domainModelsShouldNotHaveFrameworkAnnotations() {
noClasses()
.that().resideInAPackage("..domain.model..")
.should().dependOnClassesThat().resideInAnyPackage(
"io.micronaut..",
"jakarta.persistence..",
"org.springframework.."
)
.check(classes);
}Vérifie que :
- ❌ Pas de
@EntityJPA - ❌ Pas de
@SingletonMicronaut - ❌ Pas de
@ComponentSpring - ✅ Seulement
@Data,@BuilderLombok (acceptable car annotations de compilation)
6. Règles des Controllers
Pas de Logique Métier
Les Controllers ne Contiennent PAS de Logique Métier
@Test
void controllersShouldNotContainBusinessLogic() {
noClasses()
.that().resideInAPackage("..application.port.input..")
.and().haveSimpleNameEndingWith("Controller") // Seulement les *Controller
.should().dependOnClassesThat().resideInAPackage("..domain.model..")
.check(classes);
}Principe : Les controllers délèguent aux use cases, ils ne manipulent pas le domain directement.
Note importante : Cette règle s’applique uniquement aux classes *Controller. Les DTOs peuvent (et doivent) dépendre du domain pour faire le mapping.
Lancer les Tests
Maven
Via Maven
mvn test -Dtest=HexagonalArchitectureTestOu dans votre cycle de build complet :
mvn clean testCas d’Usage Réels
Cas 1: JPA dans Domain
Cas 1 : Quelqu’un ajoute une dépendance JPA dans le Domain
Code violant l’architecture :
// ❌ domain/model/TrendResult.java
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
@Entity // ❌ VIOLATION !
public class TrendResult {
@Id
private Long id;
}Test qui échoue :
domainModelsShouldNotHaveFrameworkAnnotations FAILED
Class <TrendResult> depends on <jakarta.persistence.Entity>Solution :
// ✅ domain/model/TrendResult.java
@Value
@Builder
public class TrendResult {
Long id; // Pas d'annotation JPA
}Intégration CI/CD
GitHub Actions
GitHub Actions
name: Architecture Tests
on: [push, pull_request]
jobs:
architecture-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
java-version: '21'
distribution: 'temurin'
- name: Run Architecture Tests
run: mvn test -Dtest=HexagonalArchitectureTestRésultat : Impossible de merger si l’architecture est violée ! 🚫
Ajouter une Nouvelle Règle
System.out.println
Exemple : Interdire l’utilisation de System.out.println
@Test
@DisplayName("Ne pas utiliser System.out.println (utiliser un logger)")
void shouldNotUseSystemOutPrintln() {
ArchRule rule = noClasses()
.should().callMethod(System.class, "out")
.because("Utiliser un logger (SLF4J) au lieu de System.out.println");
rule.check(classes);
}Cette règle force les développeurs à utiliser un logger au lieu de System.out.println.
Résumé
ArchUnit garantit automatiquement que votre architecture hexagonale est respectée !
| Règle | Vérifie | Empêche |
|---|---|---|
domainShouldNotDependOnAnything | Domain indépendant | Couplage au framework |
applicationShouldNotDependOnInfrastructure | Inversion de dépendances | Couplage fort |
layeredArchitectureShouldBeRespected | Architecture en couches | Violations de flux |
outputPortsShouldBeInterfaces | Ports = contrats | Implémentations dans le domain |
adaptersShouldImplementDomainPorts | Adaptateurs dépendent des ports | Adaptateurs isolés |
domainModelsShouldNotHaveFrameworkAnnotations | Domain pur | Couplage JPA/Micronaut |
controllersShouldNotContainBusinessLogic | Séparation des responsabilités | Logique dans controllers |
Notes importantes :
- ✅ Les DTOs peuvent dépendre du domain pour le mapping
- ✅ Les classes générées (Micronaut
$Definition, Lombok) sont exclues automatiquement - ✅ Les dépendances vers
java.*,lombok.*sont ignorées dans les règles de couches
Checklist
Avant de commit :
-
mvn test -Dtest=HexagonalArchitectureTestpasse ✅ - Le domain ne dépend de rien
- L’application ne dépend pas de l’infrastructure
- Les adaptateurs implémentent des interfaces du domain
- Les controllers ne contiennent pas de logique métier
- Les DTOs sont dans le bon package
Prochaines étapes
Maintenant que votre architecture est protégée, explorez comment tester votre code efficacement.
- Tests Unitaires → - Tester la logique métier
- Tests d’Intégration → - Tester les endpoints
- Pièges Courants → - Éviter les erreurs