Après avoir créé de faux objets manuellement, nous allons maintenant voir comment le faire grâce à un framework de simulacre.
Moq est un framework de simulacre qui va nous permettre de créer de faux objets facilement. Facile à mettre en place, simple d’utilisation, c’est l’outil qu’il nous faut.
Nos tests ne compilent plus, et pour cause, il nous manque une dépendance dans la classe Jeu
. Pour créer cette fausse dépendance avec Moq, il suffit de faire :
// Arrange
var fournisseurMeteo = Mock.Of<IFournisseurMeteo>();
Jeu jeu = new Jeu(fournisseurMeteo);
// Act
var resultat = jeu.Tour(6, 1);
// Assert
resultat.Should().Be(Resultat.Gagne);
jeu.Heros.Points.Should().Be(1);
jeu.Heros.PointDeVies.Should().Be(15);
Moq est capable de créer de fausses implémentations à partir d’une abstraction (interface ou classe abstraite avec le mot-clé virtual
). Il suffit de l’utiliser avec la méthode Mock.Of<>
. Dynamiquement, Moq crée une classe proxy et nous permettra d’utiliser cet objet comme si c’était le vrai.
Dans notre cas de test, fournisseurMeteo
n’est jamais utilisé, car il ne sert que lorsque le joueur a un lancer de dé inférieur à celui du monstre. Donc nous ne voyons pas directement l’effet de ce faux objet (à part qu’il a résolu nos problèmes de compilation).
Mais voyons ce qu’il se passe dans le cas du dernier test :
[TestMethod]
[Description("Etant donné un tour de jeu, lorsque j'ai un lancer inférieur au second, alors le résultat est perdu, sans point et en perdant des points de vie")]
public void Tour_AvecUnDeInferieurAuSecond_RetournePerduSansPointEnPerdantDesPointsDeVie()
{
// Arrange
var fournisseurMeteo = Mock.Of<IFournisseurMeteo>();
Jeu jeu = new Jeu(fournisseurMeteo);
// Act
var resultat = jeu.Tour(2, 4);
// Assert
resultat.Should().Be(Resultat.Perdu);
jeu.Heros.Points.Should().Be(0);
jeu.Heros.PointDeVies.Should().Be(13);
}
Si j’exécute le test, il passe.
Mais quelle donnée de météo a donc été utilisée ?
Pour le savoir, vous pouvez aller voir en debug ce qu’il se passe. Nous pouvons notamment :
voir que l’objet
_fournisseurMeteo
est d’un type bizarre, il s’agit d’un proxy ;
que la valeur renvoyée est
Meteo.Soleil
.
Alors pourquoi Meteo.Soleil
? Tout simplement parce que nous n’avons pas défini de valeur à utiliser ; alors il prend la valeur par défaut, en l’occurrence la première valeur de notre énumération.
Pour lui faire prendre une valeur spécifique, il va falloir le lui indiquer :
[TestMethod]
[Description("Etant donné un tour de jeu, lorsque j'ai un lancer inférieur au second et du vent, alors le résultat est perdu, sans points et en perdant deux fois plus de points de vie")]
public void Tour_AvecUnDeInferieurAuSecond_EtDuVent_RetournePerduSansPointEnPerdantDeuxFoisPlusDePointsDeVie()
{
// Arrange
var fournisseurMeteo = Mock.Of<IFournisseurMeteo>();
Mock.Get(fournisseurMeteo).Setup(m => m.QuelTempsFaitIl()).Returns(Meteo.Tempete);
Jeu jeu = new Jeu(fournisseurMeteo);
// Act
var resultat = jeu.Tour(2, 4);
// Assert
resultat.Should().Be(Resultat.Perdu);
jeu.Heros.Points.Should().Be(0);
jeu.Heros.PointDeVies.Should().Be(11);
}
C’est la ligne 7 qui indique que la méthode QuelTempsFaitIl
du faux objet de météo va renvoyer Meteo.Tempête
, en se moquant bien du générateur aléatoire. On peut le vérifier avec nos assertions de fin de tests : les dégâts doivent avoir doublé, donc avoir un nombre de points de vie égal à 11.
N’hésitez pas à créer le test qui vérifie ce qu’il se passe avec de la pluie. Il suffit d’utiliser :
Mock.Get(fournisseurMeteo).Setup(m => m.QuelTempsFaitIl()).Returns(Meteo.Pluie);
Vérifier que le résultat vaut à nouveau 13. En effet, nous n’avons pas de malus lorsqu’il y a de la pluie.
D’autres exemples de Moq
Moq nous permet de faire plein d’autres choses qui peuvent nous servir régulièrement dans nos tests.
Nous avons vu que nous pouvions simuler le résultat d’une méthode et forcer une valeur spécifique. Cela fonctionne lorsqu’il n’y a pas de paramètre à la méthode (comme dans notre précédent exemple), mais aussi lorsqu’il y en a.
Nous pouvons même faire prendre une valeur spécifique en fonction de la valeur du paramètre passé.
Par exemple, en considérant la classe de démonstration suivante :
public class Demo
{
public virtual int DemoMethode(int valeur)
{
return 1;
}
}
Avec les arrangements suivants, j'ai les résultats ci-dessous :
[TestMethod]
public void Moq_Exemples()
{
var fauxObjet = Mock.Of<Demo>();
Mock.Get(fauxObjet).Setup(x => x.DemoMethode(1)).Returns(4);
Mock.Get(fauxObjet).Setup(x => x.DemoMethode(6)).Returns(0);
fauxObjet.DemoMethode(6).Should().Be(0);
fauxObjet.DemoMethode(1).Should().Be(4);
fauxObjet.DemoMethode(6).Should().Be(0);
}
J’ai fait exprès de mettre la méthode avec le paramètre 6 deux fois pour montrer que l’ordre n’a pas d’influence sur le comportement.
Si la valeur du paramètre n’a pas d’intérêt, on peut utiliser la méthode It.IsAny<int>()
pour renvoyer un résultat, peu importe le paramètre de type entier qui est passé :
[TestMethod]
public void Moq_Exemples()
{
var fauxObjet = Mock.Of<Demo>();
Mock.Get(fauxObjet).Setup(x => x.DemoMethode(It.IsAny<int>())).Returns(4);
fauxObjet.DemoMethode(0).Should().Be(4);
fauxObjet.DemoMethode(1).Should().Be(4);
fauxObjet.DemoMethode(-16).Should().Be(4);
}
On peut aussi faire en sorte que les valeurs changent au fur et à mesure des appels, en utilisant SetupSequence
:
[TestMethod]
public void Moq_Exemples()
{
var fauxObjet = Mock.Of<Demo>();
Mock.Get(fauxObjet).SetupSequence(x => x.DemoMethode(It.IsAny<int>()))
.Returns(4)
.Returns(5)
.Returns(6);
fauxObjet.DemoMethode(0).Should().Be(4);
fauxObjet.DemoMethode(1).Should().Be(5);
fauxObjet.DemoMethode(-16).Should().Be(6);
}
Au premier appel, la méthode va retourner 4. Puis au second appel 5, et ensuite 6.
Ce qui veut dire que nous allons être capables de nous passer de la classe FauxDe
que nous avions écrite en utilisant :
[TestMethod]
public void Ihm_AvecUnJeuDeDonnees_LeJoueurGagne()
{
// arrange
var fausseConsole = new FausseConsole();
var fauxDe = Mock.Of<ILanceurDeDe>();
var sequence = Mock.Get(fauxDe).SetupSequence(de => de.Lance());
foreach (var lancer in 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 })
{
sequence.Returns(lancer);
}
var fournisseurMeteo = Mock.Of<IFournisseurMeteo>();
var ihm = new Ihm(fausseConsole, fauxDe, fournisseurMeteo);
// 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);
}
Ici, la variable fauxDe
a le même comportement que si l'on avait utilisé la classe FauxDe
, sauf que nous avons utilisé Moq pour lui fournir une liste de valeurs prédéfinies.
Une méthode simulée peut également lever une exception :
[TestMethod]
[ExpectedException(typeof(NotImplementedException))]
public void Moq_Exemples()
{
var fauxObjet = Mock.Of<Demo>();
Mock.Get(fauxObjet).Setup(x => x.DemoMethode(It.IsAny<int>()))
.Throws<NotImplementedException>();
fauxObjet.DemoMethode(1);
}
Moq peut faire encore beaucoup de choses, par exemple bouchonner des propriétés, ou vérifier que des méthodes ont été appelées. N’hésitez pas à consulter la documentation à ce sujet.
Résumé de la partie
Vous avez maintenant une meilleure vision de ce que sont les simulacres et les bouchons. Ils sont des éléments indispensables dès que nous avons besoin de maîtriser une dépendance imprévisible. Sans cela, les tests ont de fortes chances de renvoyer des faux positifs plus ou moins rapidement (en général, très rapidement lorsqu’il s’agit d’aléatoire !). Vous pouvez écrire vous-même vos faux objets ou bien utiliser simplement des frameworks de simulacres.