> 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/6-definir-des-proprietes.md).

# 6) Définir des propriétés

Avant de nous lancer dans notre refactoring, on peut encore aller plus loin sur nos tests afin d'améliorer encore notre confiance vis-à-vis de notre code base.

Pour ce faire on va écrire des `tests de propriétés` :

* Prendre du temps pour comprendre ce qu'est le [`Property-Based Testing`](https://xtrem-tdd.netlify.app/Flavours/pbt)
* Quelles propriétés peut-on identifier à partir de notre `Example Mapping` ?
* Ecrire des tests de propriétés en utilisant la librairie [FsCheck](https://fscheck.github.io/FsCheck/Properties.html)

<figure><img src="/files/O7NIMIGN0UB7hVMbXkMM" alt=""><figcaption><p>Step 6 : Définir des propriétés</p></figcaption></figure>

## Démarrer une partie

<figure><img src="/files/arHd2MIDMZixHevRb5eg" alt=""><figcaption><p>Démarrer une nouvelle partie de chasse</p></figcaption></figure>

On peut identifier des propriétés telles que :

```
forall(terrain contenant des galinettes, groupe de chasseurs avec chacun-e a minima 1 balle)
La partie de chasse doit démarrer avec succès
```

Ou encore :

```
forall(terrain sans galinettes, groupe de chasseurs avec au moins 1 chasseur sans balle)
La partie de chasse ne démarre pas car "pas de galinettes sur le terrain"
```

### Cas passant

* On commence par ajouter la dépendance sur `FsCheck`

```shell
dotnet add package FsCheck.xUnit
```

* On ajoute 1 test dans la classe `DemarrerUnePartieDeChasse`

```csharp
[Property] // Annotation pour xUnit
public Property Sur1TerrainAvecGalinettesEtAuMoins1ChasseurAvecTousDesBalles() =>
    Prop.ForAll(
        null,
        (terrain, chasseurs) => true
    );
```

* On doit travailler sur la génération d'un terrain valide

```csharp
private static Arbitrary<(string nom, int nbGalinettes)> terrainGenerator()
    => (from nom in Arb.Generate<string>()
        // A minima 1 galinette sur le terrain
        from nbGalinette in Gen.Choose(1, int.MaxValue)
        select (nom, nbGalinette)).ToArbitrary();
```

* Ensuite, on travaille sur la manière de générer des chasseurs valides

```csharp
private static Arbitrary<(string nom, int nbBalles)> chasseurGenerator()
    => (from nom in Arb.Generate<string>()
        // A minima 1 balle
        from nbBalles in Gen.Choose(1, int.MaxValue)
        select (nom, nbBalles)).ToArbitrary();
```

* On définit comment construire 1 groupe de chasseurs :

```csharp
private static Arbitrary<FSharpList<(string nom, int nbBalles)>> groupeDeChasseursGenerator()
    => // On définit le nombre de chasseurs dans le groupe [1; 1000]
        (from nbChasseurs in Gen.Choose(1, 1_000)
        // On utilise le nombre de chasseurs pour générer le bon nombre de chasseurs
        select chasseurGenerator().Generator.Sample(1, nbChasseurs)).ToArbitrary();
```

* On utilise les générateurs dans la propriété

```csharp
[Property]
public Property Sur1TerrainAvecGalinettesEtAuMoins1ChasseurAvecTousDesBalles() =>
    Prop.ForAll(
        terrainGenerator(),
        groupeDeChasseursGenerator(),
        (terrain, chasseurs) =>
        {
            var savedId = PartieDeChasseService.Demarrer(
                terrain,
                chasseurs.ToList()
            );
            return Repository.SavedPartieDeChasse()!.Id == savedId;
        }
    );
```

* On lance le test et vérifie les inputs générés&#x20;

<figure><img src="/files/MXwsJ6C40DWljLUx9EnQ" alt=""><figcaption><p>Input generation</p></figcaption></figure>

* On a alors 1 seul test qui va en fait valoir l'écriture de 100 tests

Pour ce test on a alors :

```csharp
private static Arbitrary<(string nom, int nbGalinettes)> terrainGenerator()
    => (from nom in Arb.Generate<string>()
        from nbGalinette in Gen.Choose(1, int.MaxValue)
        select (nom, nbGalinette)).ToArbitrary();

private static Arbitrary<(string nom, int nbBalles)> chasseurGenerator()
    => (from nom in Arb.Generate<string>()
        from nbBalles in Gen.Choose(1, int.MaxValue)
        select (nom, nbBalles)).ToArbitrary();

private static Arbitrary<FSharpList<(string nom, int nbBalles)>> groupeDeChasseursGenerator()
    => (from nbChasseurs in Gen.Choose(1, 1_000)
        select chasseurGenerator().Generator.Sample(1, nbChasseurs)).ToArbitrary();

[Property]
public Property Sur1TerrainAvecGalinettesEtAuMoins1ChasseurAvecTousDesBalles() =>
    Prop.ForAll(
        terrainGenerator(),
        groupeDeChasseursGenerator(),
        (terrain, chasseurs) => DémarreLaPartieAvecSuccès(terrain, chasseurs));

private bool DémarreLaPartieAvecSuccès((string nom, int nbGalinettes) terrain,
    IEnumerable<(string nom, int nbBalles)> chasseurs)
    => PartieDeChasseService.Demarrer(
        terrain,
        chasseurs.ToList()) == Repository.SavedPartieDeChasse()!.Id;
```

Cette propriété est complémentaire au test `DemarrerUnePartieDeChasse.AvecPlusieursChasseurs` qui valide la bonne instantiation de l'objet `PartieDeChasse` à partir d'un exemple.&#x20;

Dans la propriété, on valide que la partie démarre sans se soucier de l'instantiation de la partie au sens du `Domain`.

### Cas non passant

Concernant les cas `non-passants`, nous allons les remplacer par des tests de propriétés.

* On commence par ce test :

```csharp
[Fact]
public void SansChasseurs()
    => ExecuteAndAssertThrow<ImpossibleDeDémarrerUnePartieSansChasseur>(
        s => s.Demarrer(("Pitibon sur Sauldre", 3), new List<(string, int)>()),
        p => p.Should().BeNull()
    );
```

* On le change en `Property`

```csharp
[Property]
public Property SansChasseursSurNimporteQuelTerrainRicheEnGalinette()
    => Prop.ForAll(
        // On réutilise le générateur de terrains riches en galinettes
        terrainGenerator(),
        terrain =>
        {
            // On va refactorer pour réutiliser cette logique
            try
            {
                PartieDeChasseService.Demarrer(
                    terrain,
                    new List<(string, int)>());

                return false;
            }
            catch (ImpossibleDeDémarrerUnePartieSansChasseur)
            {
                return Repository.SavedPartieDeChasse() == null;
            }
        });
```

* On crée une méthode qui va lancer l'action de manière `safe` et valider la lancement de l'exception

```csharp
protected bool MustFailWith<TException>(Action action, Func<PartieDeChasse?, bool>? assert = null)
    where TException : Exception
{
    try
    {
        action();
        return false;
    }
    catch (TException)
    {
        return assert?.Invoke(SavedPartieDeChasse()) ?? true;
    }
}

private bool EchoueAvec<TException>(
                (string nom, int nbGalinettes) terrain,
                IEnumerable<(string nom, int nbBalles)> chasseurs,
                Func<PartieDeChasse?, bool>? assert = null) where TException : Exception
                => MustFailWith<TException>(() => PartieDeChasseService.Demarrer(terrain, chasseurs.ToList()), assert);

[Property]
public Property SansChasseursSurNimporteQuelTerrainRicheEnGalinette()
    => ForAll(
        terrainRicheEnGalinettesGenerator(),
        terrain =>
            EchoueAvec<ImpossibleDeDémarrerUnePartieSansChasseur>(
                terrain,
                PasDeChasseurs,
                savedPartieDeChasse => savedPartieDeChasse == null)
```

### Autres propriétés

D'autres propriétés pourraient être définies sur d'autres classes de tests.

Nouveau rapport `SonarCloud` disponible [ici](https://sonarcloud.io/summary/overall?id=ythirion_refactoring-du-bouchonnois\&branch=steps%2F06-properties).

### Reflect

* Que pensez vous de cette technique ?
* Quelles sont ses avantages ?
* Comment vous pourriez l'utiliser ?

<figure><img src="/files/LrGSvzsLgQhW8HLbPPFu" 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/6-definir-des-proprietes.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.
