Les tests d'architecture avec ArchUnitNET
Publi� le
Avez-vous déjà travaillé sur un projet où, petit à petit, l'architecture s'est dégradée ? Un développeur pressé qui ajoute une dépendance "temporaire" entre deux couches, un autre qui nomme sa classe n'importe comment... Et un beau jour, vous vous retrouvez avec un beau bazar qu'on nommera "du legacy".
C'est exactement ce que les tests d'architecture permettent d'éviter.
Le problème : l'érosion architecturale
Quand on démarre un projet, on définit généralement une architecture claire : Clean Architecture, Hexagonal, Onion... On trace des schémas, on explique les règles aux équipes. Mais avec le temps, ces règles s'oublient. Les nouveaux arrivants ne les connaissent pas forcément, et même les anciens peuvent faire des erreurs sous la pression des deadlines.
Le résultat ? Une architecture qui se dégrade progressivement. C'est ce qu'on appelle l'érosion architecturale ou encore, selon Wikipédia, la dégradation logicielle en bon français. Et une fois que le mal est fait, c'est très coûteux à corriger.
La solution : automatiser la validation
Et si on pouvait valider automatiquement que notre code respecte les règles architecturales définies ? C'est exactement ce que permettent les tests d'architecture.
L'idée est simple : écrire des tests qui vérifient que :
- Les dépendances entre couches sont correctes
- Les conventions de nommage sont respectées
- Les classes sont placées dans les bons namespaces
- Et bien plus encore...
Ces tests s'exécutent en local mais surtout dans votre CI/CD, comme n'importe quel autre test. Si quelqu'un viole une règle architecturale, le build échoue. Impossible de merger du code qui casse l'architecture !
ArchUnitNET : l'outil parfait pour .NET
Pour les projets .NET, ArchUnitNET est la référence. C'est un port de la célèbre librairie Java ArchUnit. Elle s'intègre parfaitement avec xUnit (ou NUnit) et propose une API très lisible.
Installation
Commencez par créer un projet de test dédié et ajoutez le package ArchUnitNET :
Install-Package TngTech.ArchUnitNET.xUnitSi vous n'utilisez pas xUnit, d'autres packages existent :
Install-Package TngTech.ArchUnitNET
Install-Package TngTech.ArchUnitNET.NUnit
Install-Package TngTech.ArchUnitNET.MSTestV2N'oubliez pas d'ajouter des références vers tous les projets que vous souhaitez analyser.
Charger l'architecture
La première étape consiste à charger les assemblies que vous voulez analyser. Pour faire ça, il suffit de référencer un type présent dans chacune des assemblies :
private static readonly Architecture Architecture = new ArchLoader()
.LoadAssemblies(
typeof(Domain.Entities.MonEntity).Assembly,
typeof(Application.Services.MonService).Assembly,
typeof(Infrastructure.Persistence.MonRepository).Assembly
.Build();Cet objet Architecture sera ensuite utilisée par tous vos tests.
Exemples concrets
Passons aux choses sérieuses avec des exemples.
1. Valider les dépendances entre couches
En Clean Architecture, le Domain ne doit dépendre de rien d'autre. Voici comment le vérifier :
private static readonly IObjectProvider<IType> DomainLayer =
Types().That().ResideInNamespace("MonProjet.Domain.*", useRegularExpressions: true)
.As("Domain Layer");
private static readonly IObjectProvider<IType> ApplicationLayer =
Types().That().ResideInNamespace("MonProjet.Application.*", useRegularExpressions: true)
.As("Application Layer");
[Fact]
public void DomainLayer_ShouldNotDependOn_ApplicationLayer()
{
var rule = Types()
.That()
.Are(DomainLayer)
.Should()
.NotDependOnAny(ApplicationLayer)
.Because("Domain layer should not depend on Application layer (Clean Architecture)");
rule.Check(Architecture);
}Ce test échouera si quelqu'un ajoute un using vers l'Application depuis le Domain. Simple et efficace !
Vous pouvez créer des tests similaires pour toutes les règles de dépendance :
- Le Domain ne dépend de rien
- L'Application ne dépend que du Domain
- L'Infrastructure peut dépendre du Domain et de l'Application
2. Conventions de nommage
Les conventions de nommage sont souvent négligées, mais elles facilitent énormément la navigation dans le code. Voici comment les renforcer :
[Fact]
public void Interfaces_ShouldStartWith_I()
{
var rule = Interfaces()
.Should()
.HaveNameStartingWith("I")
.Because("Interface names should start with 'I' prefix by convention");
rule.Check(Architecture);
}
[Fact]
public void ServiceClasses_ShouldEndWith_Service()
{
var rule = Classes()
.That()
.ResideInNamespace(".*Services.*", useRegularExpressions: true)
.Should()
.HaveNameEndingWith("Service")
.Because("Service class names should end with 'Service'");
rule.Check(Architecture);
}3. Placement des classes
Où placer les interfaces de Repository ? Dans le Domain bien sûr, pour respecter l'inversion de dépendance :
[Fact]
public void RepositoryInterfaces_ShouldResideInDomainOrRepositoriesNamespace()
{
var rule = Interfaces()
.That()
.HaveNameEndingWith("Repository")
.Should()
.ResideInNamespace(".*Repositories.*", useRegularExpressions: true)
.Because("Repository interfaces should be defined in a Repositories namespace");
rule.Check(Architecture);
}
[Fact]
public void RepositoryImplementations_ShouldBeInInfrastructureLayer()
{
var rule = Classes()
.That()
.HaveNameEndingWith("Repository")
.And()
.AreNotAbstract()
.Should()
.ResideInNamespace(".*Infrastructure.*", useRegularExpressions: true)
.Because("Repository implementations should be in Infrastructure layer");
rule.Check(Architecture);
}Organisation des tests
Je recommande d'organiser vos tests d'architecture en plusieurs fichiers selon leur préoccupation :
- LayerDependencyTests.cs : règles de dépendances entre couches
- NamingConventionTests.cs : conventions de nommage
- StructuralTests.cs : placement des classes, visibilité, etc.
Cette organisation facilite la maintenance et permet à chacun de comprendre rapidement quelles règles sont en place.
Bonnes pratiques
Quelques conseils tirés de mon expérience :
Commencez petit
N'essayez pas de tout couvrir dès le départ. Commencez par les règles les plus importantes (dépendances entre couches) et ajoutez progressivement.
Utilisez .Because()
Expliquez toujours pourquoi une règle existe. Ça aide à comprendre l'intention et à ne pas simplement contourner le test.
Utilisez les regex pour les namespaces
Les namespaces exacts sont fragiles. Préférez les patterns regex comme ".*Domain.*" pour être plus flexible.
Exécutez dans la CI
Ces tests n'ont de valeur que s'ils sont bloquants. Intégrez-les à votre pipeline CI/CD.
Documentez les exceptions
Si vous devez ignorer certains cas, documentez pourquoi. Utilisez les mécanismes d'exclusion d'ArchUnitNET plutôt que de supprimer le test.
Conclusion
Les tests d'architecture sont un investissement qui paie sur le long terme. Ils :
- Documentent votre architecture
- Protègent de l'érosion architecturale
- Éduquent les nouveaux membres de l'équipe
- Automatisent des vérifications autrement manuelles
Le coût de mise en place est faible comparé aux bénéfices. Sur un projet existant, vous pouvez même utiliser ces tests pour identifier les violations actuelles avant de les corriger progressivement.
Alors, qu'attendez-vous pour ajouter des tests d'architecture à votre projet ?