• 12 heures
  • Facile

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 16/12/2019

Créez un faux objet pour simuler des comportements

Nous avons déjà parlé du principe d’un framework de simulacre dans la partie précédente. Un framework de simulacre fournit un moyen de tester une méthode, une classe, etc., en l’isolant du reste du système.

Nous avons cité le cas qui permettait de récupérer la météo du jour. Nous avons vu qu'il fallait bouchonner pour éviter d’avoir un test qui passe quand il fait beau et un test qui échoue quand il pleut.

L’aléatoire, c'est un super terrain de jeu pour les bouchons. Cela tombe bien, nous n’avons pas testé la classe qui pilote le jeu : la classe  Ihm  . Pour pouvoir la tester correctement et valider un scénario complet, il faut pouvoir maîtriser le lancer du dé.

Et là, ça se complique. Comment maîtriser l’aléatoire ?

Créez un faux objet

Rien de plus simple, nous allons créer un faux dé, qui ne renvoie que ce qui nous arrange. On pourrait envisager d’avoir une liste prédéfinie de jets. Ainsi, nous pourrions connaître le résultat final et nous assurer que le scénario est correct.

Par exemple, créons un faux dé :

public class FauxDe
{
    private readonly int[] _listeDeJets;
    private int _compteur;

    public FauxDe()
    {
        _listeDeJets = new [] { 4, 5, 1, 1, 4, 3, 5, 6, 6, 6, 1, 2, 4, 2, 3, 2, 6, 4, 5, 1, 1, 4, 3, 5, 6, 6, 6, 1, 2, 4, 2, 3, 2, 6 };
        _compteur = 0;
    }

    public int Lance()
    {
        int tirage = _listeDeJets[_compteur];
        _compteur++;
        return tirage;
    }
}

En utilisant un tableau avec des valeurs prédéfinies, nous avons désormais un système qui va nous permettre de prédire la suite des lancers.

Il suffit de remplacer tous les endroits où nous utilisons la classe  De  par la classe  FauxDe , et notamment dans la classe  Ihm  :

public class Ihm
{
    public void Demarre()
    {
        FauxDe de = new FauxDe();
        var jeu = new Jeu();
        Console.WriteLine($"A l'attaque : points/vie {jeu.Heros.Points}/{jeu.Heros.PointDeVies}");
        while (jeu.Heros.PointDeVies > 0)
        {
            var resultat = jeu.Tour(de.Lance(), de.Lance());
            switch (resultat)
            {
                case Resultat.Gagne:
                    Console.Write($"Monstre battu");
                    break;
                case Resultat.Perdu:
                    Console.Write($"Combat perdu");
                    break;
            }
            Console.WriteLine($": points/vie {jeu.Heros.Points}/{jeu.Heros.PointDeVies}");
        }
    }
}

Le remplacement s’est fait à la ligne 5.

Maintenant, il devient facile de tester cette classe. Ajoutons une nouvelle classe de tests dans notre projet de tests pour tester la classe  Ihm  :

[TestClass]
public class IhmTests
{
}

Voici notre méthode de test :

[TestClass]
public class IhmTests
{
    [TestMethod]
    public void Ihm_AvecUnJeuDeDonnees_LeJoueurGagne()
    {
        // arrange
        Ihm ihm = new Ihm();

        // act
        ihm.Demarre();

        // assert
    }
}

Les tests passent. Mais...

On teste quoi, d’ailleurs ?

La méthode  Demarre()  est exécutée, mais, peu importe ce qu'il se passe derrière, finalement, le test passera toujours. Sauf si une exception est levée.

Il se trouve que l’on ne peut rien tester ici, car ce que l’on veut tester, c’est le résultat de ce qui s’affiche. Nous avons besoin d’un test d’interface, et pas d’un test unitaire.

Quoique… Nous pourrions peut-être envisager de modifier la classe  Ihm  afin d’exposer la variable de jeu et de voir les informations du héros ? Qu’en pensez-vous ?

STOP ! Retirez tout de suite cette idée de votre tête, car c’est une mauvaise idée. Nous ne devons pas modifier notre code et introduire des éléments qui ne servent qu’à des fins de test. En faisant cela, nous ajoutons de la complexité et du code alors que cela ne servirait à rien dans notre code de production. C’est une mauvaise pratique à bannir.

D’ailleurs, nous avons déjà modifié le code de production en remplaçant le  De  par un  FauxDe . Encore une mauvaise idée, notre programme est désormais faussé et ne fonctionne plus que pour les tests.

Pourquoi nous retrouvons-nous dans cette situation ?

Parce nous avons mal écrit notre code, sans respecter les bonnes pratiques de testabilité.

À noter que si nous avions suivi la pratique TDD, alors nous n’aurions sans doute pas eu ce problème, car le code aurait été écrit, par définition, pour être testable.

Remaniez le code

Mais ne vous inquiétez pas, tout n’est pas perdu. Nous allons remanier un petit peu notre code pour pouvoir le rendre testable et bénéficier de tout l’intérêt d’avoir un langage de programmation orienté objet : le polymorphisme.

Pour ce faire, nous allons utiliser le principe d’inversion de dépendance en introduisant une abstraction entre des classes qui sont fortement couplées.

En l’occurrence, nous avons une forte dépendance entre la classe  Ihm  et la classe  De . De même, pour notre problématique de tests, on se rend compte que la liaison est trop forte entre la classe  Ihm  et la méthode Console.WriteLine  qui écrit sur la console.

Et en pratique ? Nous allons faire deux choses :

  1. Premièrement, nous allons extraire l’écriture sur la console dans une nouvelle classe et introduire une abstraction.

  2. Deuxièmement, nous allons à nouveau introduire une abstraction pour notre classe de dé.

Et quelle abstraction choisir ? Une interface fera parfaitement l’affaire.

C’est parti ! Voici une classe permettant d’écrire dans la console accompagnée de son interface :

public interface IConsole
{
    void Ecrire(string message);
    void EcrireLigne(string message);
}

public class ConsoleDeSortie : IConsole
{
    public void Ecrire(string message)
    {
        Console.Write(message);
    }

    public void EcrireLigne(string message)
    {
        Console.WriteLine(message);
    }
}

Alors, nous changeons la classe  Ihm  par :

public class Ihm
{
    private readonly IConsole _console;

    public Ihm(IConsole console)
    {
        _console = console;
    }

    public void Demarre()
    {
        FauxDe de = new FauxDe();
        var jeu = new Jeu();
        _console.EcrireLigne($"A l'attaque : points/vie {jeu.Heros.Points}/{jeu.Heros.PointDeVies}");
        while (jeu.Heros.PointDeVies > 0)
        {
            var resultat = jeu.Tour(de.Lance(), de.Lance());
            switch (resultat)
            {
                case Resultat.Gagne:
                    _console.Ecrire($"Monstre battu");
                    break;
                case Resultat.Perdu:
                    _console.Ecrire($"Combat perdu");
                    break;
            }
            _console.EcrireLigne($": points/vie {jeu.Heros.Points}/{jeu.Heros.PointDeVies}");
        }
    }
}

Il y a plusieurs choses à remarquer.

Premièrement, j’ai créé un constructeur dans lequel je vais fournir une dépendance vers une classe permettant d’écrire dans la console grâce à l’interface  IConsole  . La classe  Ihm  ne sait pas qui va écrire dans la console, ni même comment. Et ce n’est pas son problème. Tout ce qu’elle a besoin de savoir, c’est que l’on va lui fournir quelque chose qui saura écrire dans la console.

Un avantage se dessine : il n’y a pas de couplage entre les deux classes et l'on pourra changer dynamiquement le comportement d’écriture sur la console en fonction de nos besoins. Concrètement, nous utiliserons la vraie classe  ConsoleDeSortie  dans notre programme et nous utiliserons une fausse classe dans nos tests, qui implémentera bien sûr l’interface  IConsole .

Ensuite, j’ai bien sûr remplacé l’utilisation de  Console.Write(Line)  par cette nouvelle classe, qui finalement fera la même chose.

Cette façon de récupérer une dépendance via le constructeur nous force à faire l’instanciation de la classe  ConsoleDeSortie  directement au niveau de la méthode  Main  :

static void Main(string[] args)
{
    var ihm = new Ihm(new ConsoleDeSortie());
    ihm.Demarre();
}

Cette étape devient une phase de composition. (En langage d’injection de dépendances, on appelle cela le Composition Root.)

Cependant, nous voyons encore la classe  FauxDe  directement dans le code de la méthode  Demarre()  . Nous allons utiliser le même principe et créer une interface que vont implémenter nos deux classes de dé :

public interface ILanceurDeDe
{
    int Lance();
}

public class De : ILanceurDeDe
{
    private Random random;

    public De()
    {
        random = new Random();
    }

    public int Lance()
    {
        return random.Next(1, 7);
    }
}

public class FauxDe : ILanceurDeDe
{
    private readonly int[] _listeDeJets;
    private int _compteur;

    public FauxDe()
    {
        _listeDeJets = new[] { 4, 5, 1, 1, 4, 3, 5, 6, 6, 6, 1, 2, 4, 2, 3, 2, 6, 4, 5, 1, 1, 4, 3, 5, 6, 6, 6, 1, 2, 4, 2, 3, 2, 6 };
        _compteur = 0;
    }

    public int Lance()
    {
        int tirage = _listeDeJets[_compteur];
        _compteur++;
        return tirage;
    }
}

Ce qui va nous permettre de remplacer le  FauxDe  dans la classe  Ihm  :

public class Ihm
{
    private readonly IConsole _console;
    private readonly ILanceurDeDe _lanceurDeDe;

    public Ihm(IConsole console, ILanceurDeDe lanceurDeDe)
    {
        _console = console;
        _lanceurDeDe = lanceurDeDe;
    }

    public void Demarre()
    {
        var jeu = new Jeu();
        _console.EcrireLigne($"A l'attaque : points/vie {jeu.Heros.Points}/{jeu.Heros.PointDeVies}");
        while (jeu.Heros.PointDeVies > 0)
        {
            var resultat = jeu.Tour(_lanceurDeDe.Lance(), _lanceurDeDe.Lance());
            switch (resultat)
            {
                case Resultat.Gagne:
                    _console.Ecrire($"Monstre battu");
                    break;
                case Resultat.Perdu:
                    _console.Ecrire($"Combat perdu");
                    break;
            }
            _console.EcrireLigne($": points/vie {jeu.Heros.Points}/{jeu.Heros.PointDeVies}");
        }
    }
}

La partie intéressante se trouve en haut. Nous recevons une dépendance  ILanceurDeDe  directement dans le constructeur et nous utilisons celle-ci pour nous fournir deux tirages lors du tour de jeu (ligne 18).

Nous devons maintenant modifier la phase de composition pour fournir la vraie implémentation du lanceur de dé :

static void Main(string[] args)
{
    var ihm = new Ihm(new ConsoleDeSortie(), new De());
    ihm.Demarre();
}

Si nous relançons notre programme, il est à nouveau fonctionnel. Par contre, le test que nous avons écrit précédemment ne compile plus. Cela tombe bien, car nous allons le modifier.

Mais avant cela, je vous propose de déplacer la classe  FauxDe  dans le projet de tests. En effet, cette classe n’a rien à faire dans notre jeu. Elle n’a d’intérêt qu’en tant que bouchon pour nos tests. (L’interface, quant à elle, reste bien sûr dans le projet de jeu.)

C’est le moment également de créer une fausse classe d’écriture sur la console, dans le projet de test, bien sûr :

public class FausseConsole : IConsole
{
    public StringBuilder StringBuilder = new StringBuilder();

    public void Ecrire(string message)
    {
        StringBuilder.Append(message);
    }

    public void EcrireLigne(string message)
    {
        StringBuilder.AppendLine(message);
    }
}

Son but est de stocker tout ce qui aurait dû être écrit dans la console, dans une grosse chaîne de caractères, grâce à la classe  StringBuilder .

Passons maintenant à la correction du test :

[TestClass]
public class IhmTests
{
    [TestMethod]
    public void Ihm_AvecUnJeuDeDonnees_LeJoueurGagne()
    {
        // arrange
        var fausseConsole = new FausseConsole();
        var ihm = new Ihm(fausseConsole, new FauxDe());

        // act
        ihm.Demarre();

        // assert
        var resultat = fausseConsole.StringBuilder.ToString();
        resultat.Should().StartWith("A l'attaque : points/vie 0/15");
        resultat.Should().EndWith("Combat perdu: points/vie 9/0\r\n");
        resultat.Should().HaveLength(560);
    }
}

Il faut bien sûr utiliser nos deux fausses implémentations dans le constructeur de la classe que l’on souhaite tester (phase "arrange"). On démarre le jeu (phase "act"). Et ensuite, on vérifie le résultat de l’exécution (phase "assert"). Ceci est possible grâce à notre fausse classe console qui a enregistré tout ce qui s’est passé.

Ce test est idempotent, car nous avons maîtrisé la dépendance au hasard avec un résultat que l’on peut prédire ; nous pouvons donc le rejouer à l’infini en nous jouant bien du hasard ! Et le résultat est bien conforme à ce que mon tirage prévoyait.

Ce remaniement a été un peu laborieux, mais ne valait-il pas le coup ? Grâce à lui, nous pouvons mieux tester notre jeu et être certains que nos futurs développements n’introduiront pas de régression. Nous sommes confiants, sereins et motivés pour continuer.

Merci les bouchons. 🙂

Poursuivez le développement du jeu

Vous avez plein d’idées pour améliorer notre jeu ?

Moi aussi !

Combattre un monstre fictif, c’est bien 5 minutes, mais ce serait plus sympa si la météo pouvait s’en mêler. Lançons nous dans une évolution du tour de jeu ! 😄

L’idée est de faire en sorte que la perte de points de vie pour le héros soit plus importante quand il y a de la tempête. Il suffit de changer le tour de jeu pour introduire cela :

public Resultat Tour(int deHeros, int deMonstre)
{
    if (GagneLeCombat(deHeros, deMonstre))
    {
        Heros.GagneUnCombat();
        return Resultat.Gagne;
    }
    else
    {
        var temps = _fournisseurMeteo.QuelTempsFaitIl();
        if (temps == Meteo.Tempete)
            Heros.PerdsUnCombat(2 * (deMonstre - deHeros));
        else
            Heros.PerdsUnCombat(deMonstre - deHeros);
        return Resultat.Perdu;
    }
}

public enum Meteo
{
    Soleil,
    Pluie,
    Tempete
}

Nous demandons à un fournisseur météo le temps qu’il fait (ligne 10). Et si c’est la tempête, alors les points de vie à retirer sont doublés.

Étant un fan inconditionnel du hasard, notre fournisseur météo va piocher la météo au hasard. Par exemple :

public interface IFournisseurMeteo
{
    Meteo QuelTempsFaitIl();
}

public class FournisseurMeteo : IFournisseurMeteo
{
    private readonly Random _random;

    public FournisseurMeteo()
    {
        _random = new Random();
    }

    public Meteo QuelTempsFaitIl()
    {
        var tirage = _random.Next(0, 21);
        if (tirage < 10)
            return Meteo.Soleil;
        if (tirage < 20)
            return Meteo.Pluie;
        return Meteo.Tempete;
    }
}

Une implémentation bien naïve, mais peu importe. Ce qui est important, c’est que nous allons avoir un nouveau venu dans les dépendances de la classe  Jeu  :

public class Jeu
{
    private readonly IFournisseurMeteo _fournisseurMeteo;

    public Heros Heros { get; }

    public Jeu(IFournisseurMeteo fournisseurMeteo)
    {
        Heros = new Heros(15);
        _fournisseurMeteo = fournisseurMeteo;
    }
    ...
}

Bon, c’est très bien. A priori, vu notre talent de développeur, le code ne peut que fonctionner. Mais encore faudrait-il en être certain...

Il faut vraiment ? 🧐

Oui, bien sûr, il faut des tests ! Cela tombe bien, on en a déjà, mais ils ne compilent plus. 🙁

Nous pourrions donc envisager de faire comme plus haut, en créant une fausse classe qui se déguise en fournisseur de météo. Mais ça, c’était sans compter sur le prochain chapitre, qui va nous emmener dans une nouvelle façon de créer de faux objets.

Exemple de certificat de réussite
Exemple de certificat de réussite