5) "Approve Everything"
Il y a quelques tests pour lesquels nous avons énormément de lignes d'assertions. Nous allons les retravailler afin de les transformer en Approval Tests
.
Prendre du temps pour comprendre ce qui se cache derrière cette notion d'Approval Testing
Identifier des tests sur lesquels on pourrait utiliser cette technique
Refactorer un test existant en utilisant la librairie Verify

Identification des tests
On pourrait utiliser cette technique pour les tests suivants :
DemarrerUnePartieDeChasse.AvecPlusieursChasseurs
Limitera les asserts à une seule ligne
Moins de maintenance et assertions plus lisibles
ConsulterStatus
:QuandLaPartieVientDeDémarrer
/QuandLaPartieEstTerminée
ScenarioTests.DéroulerUnePartie
On valide le contenu d'un
string
Cela évitera de stocker ce string dans le code (sera stocké sous forme de ressource)
Refactorer ScenarioTests.DéroulerUnePartie
ScenarioTests.DéroulerUnePartie
On commence par ajouter la dépendance sur notre librairie d'Approval Testing
dotnet add package Verify.xUnit
On peut ensuite extraire le contenu de l'assertion dans un fichier "Approved" ici "verified"
On crée un fichier appelé :
ScenarioTests.DéroulerUnePartie.verified.txt
([Nom de la classe de tests].[Nom du test].verified.txt
)C'est sur base de ce fichier que l'assertion se fera via
Verify

On transforme le test en
Approval Test
enAjoutant l'annotation
UsesVerify
sur la classe de testChangeant la méthode de test pour que celle-ci renvoie une
Task
[UsesVerify]
public class ScenarioTests
{
[Fact]
public Task DéroulerUnePartie()
{
var time = new DateTime(2024, 4, 25, 9, 0, 0);
var repository = new PartieDeChasseRepositoryForTests();
var service = new PartieDeChasseService(repository, () => time);
var chasseurs = new List<(string, int)>
{
("Dédé", 20),
("Bernard", 8),
("Robert", 12)
};
var terrainDeChasse = ("Pitibon sur Sauldre", 4);
var id = service.Demarrer(
terrainDeChasse,
chasseurs
);
time = time.Add(TimeSpan.FromMinutes(10));
service.Tirer(id, "Dédé");
time = time.Add(TimeSpan.FromMinutes(30));
service.TirerSurUneGalinette(id, "Robert");
time = time.Add(TimeSpan.FromMinutes(20));
service.PrendreLapéro(id);
time = time.Add(TimeSpan.FromHours(1));
service.ReprendreLaPartie(id);
time = time.Add(TimeSpan.FromMinutes(2));
service.Tirer(id, "Bernard");
time = time.Add(TimeSpan.FromMinutes(1));
service.Tirer(id, "Bernard");
time = time.Add(TimeSpan.FromMinutes(1));
service.TirerSurUneGalinette(id, "Dédé");
time = time.Add(TimeSpan.FromMinutes(26));
service.TirerSurUneGalinette(id, "Robert");
time = time.Add(TimeSpan.FromMinutes(10));
service.PrendreLapéro(id);
time = time.Add(TimeSpan.FromMinutes(170));
service.ReprendreLaPartie(id);
time = time.Add(TimeSpan.FromMinutes(11));
service.Tirer(id, "Bernard");
time = time.Add(TimeSpan.FromSeconds(1));
service.Tirer(id, "Bernard");
time = time.Add(TimeSpan.FromSeconds(1));
service.Tirer(id, "Bernard");
time = time.Add(TimeSpan.FromSeconds(1));
service.Tirer(id, "Bernard");
time = time.Add(TimeSpan.FromSeconds(1));
service.Tirer(id, "Bernard");
time = time.Add(TimeSpan.FromSeconds(1));
service.Tirer(id, "Bernard");
time = time.Add(TimeSpan.FromSeconds(1));
try
{
service.Tirer(id, "Bernard");
}
catch (TasPlusDeBallesMonVieuxChasseALaMain)
{
}
time = time.Add(TimeSpan.FromMinutes(19));
service.TirerSurUneGalinette(id, "Robert");
time = time.Add(TimeSpan.FromMinutes(30));
service.TerminerLaPartie(id);
// retourne le résultat de la méthode `Verify`
return Verify(service.ConsulterStatus(id));
}
}
Le test passe du premier coup 👌
On va faire en sorte de le faire passer au rouge : ne jamais croire un test qu'on a pas vu échouer
...
Pour cela le plus simple est de changer le fichier verified
.
Notre Approval Test
échoue, notre outil de comparaison de fichier va s'ouvrir :

Dès lors nous avons une arborescence de fichiers ressemblant à celà :

Un élément important quand on utilise une librairie de ce genre, ajouter les fichiers received
dans le fichier .gitignore
:
# Verify
*.received.txt
Félicitations, notre premier test passe et on peut se fier à lui.
En revanche, le test n'est pas très lisible / maintenable :
Beaucoup de duplication
try / catch
videMéthode de plus de
80 loc
On va y appliquer la fameuse règle du boyscout.

Boy Scout Rule
On commence par extraire des champs à partir du test via notre
IDE

Puis on configure l'extraction

Le résultat est :
public class ScenarioTests
{
private DateTime _time = new(2024, 4, 25, 9, 0, 0);
private readonly PartieDeChasseService _service;
public ScenarioTests()
{
_service = new PartieDeChasseService(
new PartieDeChasseRepositoryForTests(),
() => _time
);
}
....
}
On va utiliser le
CommandBuilder
égalementAfin de supprimer les
string
hardcodés
var command = DémarrerUnePartieDeChasse()
.Avec(("Dédé", 20), ("Bernard", 8), ("Robert", 12))
.SurUnTerrainRicheEnGalinettes(4);
var id = _service.Demarrer(
command.Terrain,
command.Chasseurs
);
On va ensuite supprimer la duplication en faisant une extraction des constantes :
Bernard
,Robert
,Dédé
,ChasseurInconnu
Pour pouvoir les utiliser dans cette classe de test également
On les place dans un fichier
Data

Notre test ressemble désormais à cela
[Fact]
public Task DéroulerUnePartie()
{
var command = DémarrerUnePartieDeChasse()
.Avec((Data.Dédé, 20), (Data.Bernard, 8), (Data.Robert, 12))
.SurUnTerrainRicheEnGalinettes(4);
var id = _service.Demarrer(
command.Terrain,
command.Chasseurs
);
_time = _time.Add(TimeSpan.FromMinutes(10));
_service.Tirer(id, Data.Dédé);
_time = _time.Add(TimeSpan.FromMinutes(30));
_service.TirerSurUneGalinette(id, Data.Robert);
....
}
On va extraire une méthode à partir de cela en identifiant les similitudes et différences
// Ajoute du temps à _time
_time = _time.Add(TimeSpan.FromMinutes(30));
// Appelle d'une méthode sur le service
_service.TirerSurUneGalinette(id, Data.Robert);
// Ajoute du temps à _time
_time = _time.Add(TimeSpan.FromMinutes(20));
// Appelle d'une méthode sur le service
_service.PrendreLapéro(id);
On prépare notre
extraction
en décomposant le code ci-dessus en :
// Extract variable
var timeToAdd = TimeSpan.FromMinutes(10);
// Refactor l'appelle en Action
var act = () => _service.Tirer(id, Data.Dédé);
_time = _time.Add(timeToAdd);
act();
Extraction de la méthode

Puis on configure l'extraction

On l'utilise partout en s'assurant que notre test reste vert
En rendant également
safe
l'appelle à la méthodeact
[UsesVerify]
public class ScenarioTests
{
private DateTime _time = new(2024, 4, 25, 9, 0, 0);
private readonly PartieDeChasseService _service;
public ScenarioTests()
{
_service = new PartieDeChasseService(
new PartieDeChasseRepositoryForTests(),
() => _time
);
}
[Fact]
public Task DéroulerUnePartie()
{
var command = DémarrerUnePartieDeChasse()
.Avec((Data.Dédé, 20), (Data.Bernard, 8), (Data.Robert, 12))
.SurUnTerrainRicheEnGalinettes(4);
var id = _service.Demarrer(
command.Terrain,
command.Chasseurs
);
After(10.Minutes(), () => _service.Tirer(id, Data.Dédé));
After(30.Minutes(), () => _service.TirerSurUneGalinette(id, Data.Robert));
After(20.Minutes(), () => _service.PrendreLapéro(id));
After(1.Hours(), () => _service.ReprendreLaPartie(id));
After(2.Minutes(), () => _service.Tirer(id, Data.Bernard));
After(1.Minutes(), () => _service.Tirer(id, Data.Bernard));
After(1.Minutes(), () => _service.TirerSurUneGalinette(id, Data.Dédé));
After(26.Minutes(), () => _service.TirerSurUneGalinette(id, Data.Robert));
After(10.Minutes(), () => _service.PrendreLapéro(id));
After(170.Minutes(), () => _service.ReprendreLaPartie(id));
After(11.Minutes(), () => _service.Tirer(id, Data.Bernard));
After(1.Seconds(), () => _service.Tirer(id, Data.Bernard));
After(1.Seconds(), () => _service.Tirer(id, Data.Bernard));
After(1.Seconds(), () => _service.Tirer(id, Data.Bernard));
After(1.Seconds(), () => _service.Tirer(id, Data.Bernard));
After(1.Seconds(), () => _service.Tirer(id, Data.Bernard));
After(1.Seconds(), () => _service.Tirer(id, Data.Bernard));
After(19.Minutes(), () => _service.TirerSurUneGalinette(id, Data.Robert));
After(30.Minutes(), () => _service.TerminerLaPartie(id));
return Verify(_service.ConsulterStatus(id));
}
private void After(TimeSpan time, Action act)
{
_time = _time.Add(time);
try
{
act();
}
catch
{
// ignored
}
}
}
Refactorer DemarrerUnePartieDeChasse.AvecPlusieursChasseurs
DemarrerUnePartieDeChasse.AvecPlusieursChasseurs
On commence par changer le test
Ici on va "approuver" la représentation textuelle de la
PartieDeChasse
[Fact]
public Task AvecPlusieursChasseurs()
{
var command = DémarrerUnePartieDeChasse()
.Avec((Data.Dédé, 20), (Data.Bernard, 8), (Data.Robert, 12))
.SurUnTerrainRicheEnGalinettes();
PartieDeChasseService.Demarrer(
command.Terrain,
command.Chasseurs
);
return Verify(Repository.SavedPartieDeChasse());
}
Voici le résultat
Par défaut,
Verify
va scrubber les donées non déterministes (DateTime
etGuid
ici)

Concernant la date, on perd 1 assertion faites dans le test avant refactoring
On change la configuration pour ce test
[Fact]
public Task AvecPlusieursChasseurs()
{
var command = DémarrerUnePartieDeChasse()
.Avec((Data.Dédé, 20), (Data.Bernard, 8), (Data.Robert, 12))
.SurUnTerrainRicheEnGalinettes();
PartieDeChasseService.Demarrer(
command.Terrain,
command.Chasseurs
);
return Verify(Repository.SavedPartieDeChasse())
// On précise qu'on ne veut pas "scubber" les DateTime
.DontScrubDateTimes();
}
On peut maintenant approver le résultat du test qui ressemble à cela

Impact du refactoring des tests
Codescene
Après les refactorings des tests, on peut lancer une analyse codescene
pour vérifier leur impact sur l'état de notre code base :

Nous sommes passé d'une Code Health de 8,4 à 9,8 👌
Les hotspots ont changé de taille (rouges car les commits sont très récents)

Il reste 1 refactoring target
: PartieDeChasseService

SonarCloud
Rapport disponible ici.
Reflect
Que pensez vous de cette technique ?
Quels autres cas d'utilisation pouvez-vous identifier ?
Qu'est-ce que le
scrubbing
?

Last updated
Was this helpful?