> For the complete documentation index, see [llms.txt](https://yoan-thirion.gitbook.io/knowledge-base/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://yoan-thirion.gitbook.io/knowledge-base/software-craftsmanship/code-katas/refactoring-du-bouchonnois/5-approve-everything.md).

# 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](https://github.com/ythirion/approval-testing-kata#2-approval-testing)
* Identifier des tests sur lesquels on pourrait utiliser cette technique
* Refactorer un test existant en utilisant la librairie [Verify](https://github.com/VerifyTests/Verify)

<figure><img src="/files/iO1nEgTloE53u3zGevmd" alt=""><figcaption><p>Step 5 : "Approve Everything"</p></figcaption></figure>

## 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`

* On commence par ajouter la dépendance sur notre librairie d'Approval Testing

```shell
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`&#x20;

<figure><img src="/files/hqxMS3O6yW2D5ZQeaW6h" alt=""><figcaption><p>Approved Content</p></figcaption></figure>

* On transforme le test en `Approval Test` en
  * Ajoutant l'annotation `UsesVerify` sur la classe de test
  * Changeant la méthode de test pour que celle-ci renvoie une `Task`

```csharp
[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 :&#x20;

<figure><img src="/files/bknCRv82OneGsQsbWx11" alt=""><figcaption><p>Comparaison de fichiers</p></figcaption></figure>

Dès lors nous avons une arborescence de fichiers ressemblant à celà :&#x20;

<figure><img src="/files/GOjFT719KH3OGL4cOGQJ" alt="" width="356"><figcaption><p>Files</p></figcaption></figure>

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` vide
* Méthode de plus de `80 loc`

On va y appliquer la fameuse [règle du boyscout](https://deviq.com/principles/boy-scout-rule).&#x20;

<figure><img src="/files/Mu0zH0k5CAl0iAZyuXbi" alt="" width="375"><figcaption></figcaption></figure>

### Boy Scout Rule

* On commence par extraire des champs à partir du test via notre `IDE`&#x20;

<figure><img src="/files/HMqAnNtrfUTWJItS5ugg" alt="" width="375"><figcaption><p>Introduce field</p></figcaption></figure>

* Puis on configure l'extraction

<figure><img src="/files/baom5T6V884zp3uNIfAS" alt="" width="375"><figcaption><p>Configurer "Introduce Field"</p></figcaption></figure>

* Le résultat est :

```csharp
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` également
  * Afin de supprimer les `string` hardcodés

```csharp
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`

<figure><img src="/files/U6eYRknKhNwja0fcdRSa" alt="" width="375"><figcaption><p>Move to &#x3C;type></p></figcaption></figure>

* Notre test ressemble désormais à cela

```csharp
[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

```csharp
// 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 :

```csharp
// 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

<figure><img src="/files/MaYB7Yboqfk9MypwWJFR" alt="" width="240"><figcaption><p>Extract method</p></figcaption></figure>

* Puis on configure l'extraction

<figure><img src="/files/ccXA9msGTxojl1XK0hhU" alt="" width="375"><figcaption><p>Configure "Extract Method"</p></figcaption></figure>

* On l'utilise partout en s'assurant que notre test reste vert
  * En rendant également `safe` l'appelle à la méthode `act`

```csharp
[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`

* On commence par changer le test
  * Ici on va "approuver" la représentation textuelle de la `PartieDeChasse`

```csharp
[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` et `Guid` ici)

<figure><img src="/files/rpUq55ftLJ72OAu9hRdu" alt=""><figcaption><p>Scrubbed data</p></figcaption></figure>

* Concernant la date, on perd 1 assertion faites dans le test avant refactoring
  * On change la configuration pour ce test

```csharp
[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

<figure><img src="/files/j7OCncF9YoWDZM3XxlhD" alt="" width="375"><figcaption><p>Verify démarrer</p></figcaption></figure>

## 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 :&#x20;

<figure><img src="/files/EEWO2diqYOEHWhsDoHW1" alt=""><figcaption><p>Code health</p></figcaption></figure>

> 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)&#x20;

<figure><img src="/files/pmucizKJaNCi9GcXOkFT" alt=""><figcaption><p>Hotspots</p></figcaption></figure>

Il reste 1 `refactoring target` : `PartieDeChasseService`&#x20;

<figure><img src="/files/7GhmCAhBVeHNsdJZ3wlx" alt=""><figcaption><p>Refactoring target</p></figcaption></figure>

### SonarCloud

Rapport disponible [ici](https://sonarcloud.io/summary/overall?id=ythirion_refactoring-du-bouchonnois\&branch=steps%2F05-approve-everything).

## Reflect

* Que pensez vous de cette technique ?
  * Quels autres cas d'utilisation pouvez-vous identifier ?
* Qu'est-ce que le `scrubbing` ?

<figure><img src="/files/Jho5klTOoKtdv6LXfjK1" alt="" width="199"><figcaption></figcaption></figure>


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://yoan-thirion.gitbook.io/knowledge-base/software-craftsmanship/code-katas/refactoring-du-bouchonnois/5-approve-everything.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
