Ecrire des tests d'architecture en utilisant la librairie ArchUnit
Pour aller plus vite, voici une classe contenant des extensions facilitant l'écriture et le lancement de tels tests :
using ArchUnitNET.Fluent;
using ArchUnitNET.Fluent.Syntax.Elements.Types;
using ArchUnitNET.Loader;
using ArchUnitNET.xUnit;
using Bouchonnois.Service;
using static ArchUnitNET.Fluent.ArchRuleDefinition;
namespace Bouchonnois.Tests.Architecture
{
public static class ArchUnitExtensions
{
private static readonly ArchUnitNET.Domain.Architecture Architecture =
new ArchLoader()
.LoadAssemblies(typeof(PartieDeChasseService).Assembly)
.Build();
public static GivenTypesConjunction TypesInAssembly() =>
Types().That().Are(Architecture.Types);
public static void Check(this IArchRule rule) => rule.Check(Architecture);
}
// Exemple de test
public class Guidelines
{
private static GivenMethodMembersThat Methods() => MethodMembers().That().AreNoConstructors().And();
[Fact]
public void NoGetMethodShouldReturnVoid() =>
Methods()
.HaveName("Get[A-Z].*", useRegularExpressions: true).Should()
.NotHaveReturnType(typeof(void))
.Check();
}
}
Inward Dependencies
On va valider le sens des dépendances en ajoutant une nouvelle classe de tests
public class ArchitectureRules
{
[Fact]
public void ApplicationServicesRules() =>
{
// Les classes dans l'Application Services ne devraient pas dépendre de classes dans Infrastructure
}
[Fact]
public void InfrastructureRules()
{
// Quelles sont les classes de l'infrastructure ?
// Que devrions nous faire de ce qui est contenu dans Infra ?
}
[Fact]
public void DomainModelRules()
{
// Les classes dans Domain ne devraient pas dépendre de classes dans Infrastructure ou Application Services
}
}
On peut alors écrire une première règle pour notre Domain Model :
[Fact]
public void DomainModelRules() =>
DomainModel().Should()
.NotDependOnAny(ApplicationServices()).AndShould()
.NotDependOnAny(Infrastructure())
.Check();
Celle-ci échoue :
La classe Terrain se trouve dans l'Application Services alors qu'elle est une entité à part entière du Domain...
On corrige cela en déplaçant la classe :
Règle de l'Application Services
On en profite pour implémenter une règle sur l'Application Service :
[Fact]
public void ApplicationServicesRules() =>
Infrastructure().Should()
.NotDependOnAny(Infrastructure())
.Check();
Quid de l'infrastructure ?
Pour le moment nous n'avons qu'une interface de Repository (Un Port) au sein du namespace Infrastructure.
Est-ce que cela fait du sens au regard de la règle de dépendance ?
Nous allons déplacer ce port dans le Domain.
Nous pouvons tout de même implémenter une règle spécifiant que les items présents dans le namespace Repository doit implémenter l'interface IPartieDeChasseRepository :
[Fact]
public void InfrastructureRules() =>
Infrastructure().Should()
.ImplementInterface(typeof(IPartieDeChasseRepository))
.Check();
Règles d'équipe
On peut ajouter certaines règles d'équipe du genre :
Toutes les interfaces doivent commencer par I
Une méthode commençant par Get doit retourner quelque chose
...
public class Guidelines
{
private static GivenMethodMembersThat Methods() => MethodMembers().That().AreNoConstructors().And();
[Fact]
public void NoGetMethodShouldReturnVoid() =>
Methods()
.HaveName("Get[A-Z].*", useRegularExpressions: true).Should()
.NotHaveReturnType(typeof(void))
.Check();
[Fact]
public void IserAndHaserShouldReturnBooleans() =>
Methods()
.HaveName("Is[A-Z].*", useRegularExpressions: true).Or()
.HaveName("Has[A-Z].*", useRegularExpressions: true).Should()
.HaveReturnType(typeof(bool))
.Check();
[Fact]
public void SettersShouldNotReturnSomething() =>
Methods()
.HaveName("Set[A-Z].*", useRegularExpressions: true).Should()
.HaveReturnType(typeof(void))
.Check();
[Fact]
public void InterfacesShouldStartWithI() =>
Interfaces().Should()
.HaveName("^I[A-Z].*", useRegularExpressions: true)
.Because("C# convention...")
.Check();
}