Avec toutes les découvertes réalisées jusqu'à présent on a pu se rendre compte que l'architecture désirée était une architecture en Onion
:
On va s'assurer que le code actuel respecte le Design escompté :
- Prendre du temps pour comprendre ce que sont des
Architecture Unit Tests
- 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();
}
}
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 définit les couches de notre onion
:
private static GivenTypesConjunctionWithDescription ApplicationServices() =>
TypesInAssembly().And()
.ResideInNamespace("Service", true)
.As("Application Services");
private static GivenTypesConjunctionWithDescription DomainModel() =>
TypesInAssembly().And()
.ResideInNamespace("Domain", true)
.As("Domain Model");
private static GivenTypesConjunctionWithDescription Infrastructure() =>
TypesInAssembly().And()
.ResideInNamespace("Repository", true)
.As("Infrastructure");
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();
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 :
On en profite pour implémenter une règle sur l'Application Service :
[Fact]
public void ApplicationServicesRules() =>
Infrastructure().Should()
.NotDependOnAny(Infrastructure())
.Check();
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();
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();
}
Nouveau rapport SonarCloud
disponible ici.
- A quoi cette technique pourrait vous servir ?
- Quelles règles pourraient être utiles dans votre quotidien ?