Comment devriez-vous TDD un jeu Yahtzee?

36

Disons que vous écrivez un style TDD de jeu Yahtzee. Vous voulez tester la partie du code qui détermine si un jeu de cinq jets de dés est un full. Autant que je sache, lorsque vous utilisez TDD, vous suivez ces principes:

  • Écrire des tests d'abord
  • Écrivez la chose la plus simple possible qui fonctionne
  • Affiner et refactor

Donc, un test initial pourrait ressembler à quelque chose comme ça:

public void Returns_true_when_roll_is_full_house()
{
    FullHouseTester sut = new FullHouseTester();
    var actual = sut.IsFullHouse(1, 1, 1, 2, 2);

    Assert.IsTrue(actual);
}

Lorsque vous suivez la IsFullHouseprocédure "Écrivez la chose la plus simple possible qui fonctionne", vous devez maintenant écrire la méthode comme ceci:

public bool IsFullHouse(int roll1, int roll2, int roll3, int roll4, int roll5)
{
    if (roll1 == 1 && roll2 == 1 && roll3 == 1 && roll4 == 2 && roll5 == 2)
    {
        return true;
    }

    return false;
}

Cela donne un test vert mais la mise en œuvre est incomplète.

Devez-vous tester à l'unité toutes les combinaisons valides possibles (valeurs et positions) pour un full? Cela ressemble à la seule façon d'être absolument sûr que votre IsFullHousecode est complètement testé et correct, mais il semble aussi tout à fait insensé de le faire.

Comment voulez-vous tester quelque chose comme ça?

Mise à jour

Erik et Kilian soulignent que l'utilisation de littéraux dans la mise en œuvre initiale pour obtenir un test vert pourrait ne pas être la meilleure idée. J'aimerais expliquer pourquoi j'ai fait cela et que cette explication ne rentre pas dans un commentaire.

Mon expérience pratique des tests unitaires (en particulier en utilisant une approche TDD) est très limitée. Je me souviens d'avoir regardé un enregistrement de la TDcl Master Class TDD de Roy Osherove sur Tekpub. Dans l'un des épisodes, il construit un style TDD de calculateur de chaînes. La spécification complète du calculateur de chaînes peut être trouvée ici: http://osherove.com/tdd-kata-1/

Il commence par un test comme celui-ci:

public void Add_with_empty_string_should_return_zero()
{
    StringCalculator sut = new StringCalculator();
    int result = sut.Add("");

    Assert.AreEqual(0, result);
}

Cela se traduit par cette première implémentation de la Addméthode:

public int Add(string input)
{
    return 0;
}

Ensuite, ce test est ajouté:

public void Add_with_one_number_string_should_return_number()
{
    StringCalculator sut = new StringCalculator();
    int result = sut.Add("1");

    Assert.AreEqual(1, result);
}

Et la Addméthode est refactorisée:

public int Add(string input)
{
    if (input.Length == 0)
    {
        return 0;
    }

    return 1;
}

Après chaque étape, Roy dit "écris la chose la plus simple qui puisse fonctionner".

J'ai donc pensé essayer cette approche en essayant de créer un jeu Yahtzee de style TDD.

Kristof Claes
la source
8
"Écrivez la chose la plus simple possible qui fonctionne" est en réalité une abréviation; le conseil correct est "écris la chose la plus simple possible qui ne soit pas complètement invisible et qui est manifestement incorrecte qui fonctionne". Donc, non, vous ne devriez pas écrireif (roll1 == 1 && roll2 == 1 && roll3 == 1 && roll4 == 2 && roll5 == 2)
Carson63000 le 03
3
Merci d'avoir résumé la réponse d'Erik, que ce soit de manière moins argumentative ou plus civilisée.
Kristof Claes
1
"Ecrire la chose la plus simple qui fonctionne", comme @ Carson63000, est en réalité une simplification. C'est en fait dangereux de penser comme ça; cela mène à la fameuse débâcle de Sudoku TDD (google it). Quand on le suit aveuglément, TDD est en effet un braindead: vous ne pouvez pas généraliser un algorithme non trivial en faisant aveuglément "la chose la plus simple qui marche" ... vous devez réellement penser! Malheureusement, même les présumés maîtres de XP et de TDD le suivent parfois aveuglément ...
Andres F.
1
@AndresF. Notez que votre commentaire est apparu plus élevé dans les recherches Google que la plupart des commentaires sur la "débâcle de Soduko TDD" après moins de trois jours. Néanmoins, comment ne pas résoudre un sudoku, résumait le problème : TDD est synonyme de qualité et non de correction. Vous devez résoudre l’algorithme avant de commencer le codage, en particulier avec TDD. (Pas que je ne sois pas non plus un premier programmeur de code.)
Mark Hurd
1
pvv.org/~oma/TDDinC_Yahtzee_27oct2011.pdf pourrait présenter un intérêt.

Réponses:

40

Il y a déjà beaucoup de bonnes réponses à cette question et j'ai commenté et voté plusieurs d'entre elles. Néanmoins, j'aimerais ajouter quelques réflexions.

La flexibilité n'est pas pour les novices

Le PO indique clairement qu'il n'a pas l' expérience de la TDD et je pense qu'une bonne réponse doit en tenir compte. Dans la terminologie du modèle d'acquisition de compétences de Dreyfus , il est probablement un novice . Il n'y a rien de mal à être novice - nous sommes tous novices lorsque nous commençons à apprendre quelque chose de nouveau. Cependant, ce que le modèle Dreyfus explique, c’est que les novices se caractérisent par

  • adhérence rigide aux règles ou plans enseignés
  • pas d'exercice de jugement discrétionnaire

Ce n'est pas une description d'un déficit de personnalité, il n'y a donc aucune raison d'avoir honte de cela. C'est une étape que nous devons tous traverser pour apprendre quelque chose de nouveau.

Ceci est également vrai pour TDD.

Bien que je sois d’accord avec de nombreuses autres réponses, le TDD n’a pas à être dogmatique et qu’il peut parfois être plus bénéfique de travailler de manière alternative, cela n’aide pas les débutants. Comment pouvez-vous exercer un jugement discrétionnaire lorsque vous n'avez aucune expérience?

Si un novice accepte le conseil selon lequel il est parfois acceptable de ne pas faire de TDD, comment peut-il déterminer quand il est acceptable de ne pas le faire?

En l'absence d'expérience ou de conseils, la seule chose à faire pour un novice est de ne pas utiliser TDD chaque fois que cela devient trop difficile. C'est la nature humaine, mais pas un bon moyen d'apprendre.

Écoutez les tests

Sauter hors du TDD chaque fois que cela devient difficile est de rater l'un des avantages les plus importants du TDD. Les tests fournissent des informations précoces sur l'API du SUT. Si le test est difficile à écrire, c'est un signe important que le SUT est difficile à utiliser.

C’est la raison pour laquelle l’un des messages les plus importants du GOOS est le suivant: écoutez vos tests!

Dans le cas de cette question, ma première réaction en voyant l’API proposée du jeu Yahtzee et la discussion sur la combinatoire que l’on peut trouver sur cette page ont été qu’il s’agissait d’un retour important sur l’API.

Est-ce que l'API doit représenter les jets de dés comme une séquence ordonnée d'entiers? Pour moi, cette odeur d' obsession primitive . C'est pourquoi j'ai été heureux de voir la réponse de Tallseth suggérant l'introduction d'un Rollcours. Je pense que c'est une excellente suggestion.

Cependant, je pense que certains des commentaires à cette réponse se trompent. Ce que TDD suggère alors, c'est qu'une fois que vous avez eu l'idée qu'un Rollcours serait une bonne idée, vous suspendez les travaux sur le système sous test original et commencez à travailler sur le Rollcours.

Bien que je convienne que le TDD vise plus le «joyeux chemin» que les tests complets, il aide toujours à décomposer le système en unités gérables. Un Rollcours ressemble à quelque chose que vous pourriez terminer beaucoup plus facilement.

Ensuite, une fois que la Rollclasse aura suffisamment évolué, reviendriez-vous sur le SUT d’origine et définissez-le en termes d’ Rollentrées.

La suggestion d'un Test Helper n'implique pas forcément un caractère aléatoire, c'est simplement un moyen de rendre le test plus lisible.

Une autre façon d’aborder et de modéliser les entrées en termes d’ Rollinstances serait d’introduire un générateur de données de test .

Red / Green / Refactor est un processus en trois étapes

Bien que je sois d’accord avec l’opinion générale selon laquelle (si vous êtes suffisamment expérimenté en TDD), vous n’avez pas besoin de vous en tenir rigoureusement, je pense que c’est plutôt mauvais conseil dans le cas d’un exercice de Yahtzee. Bien que je ne connaisse pas les détails des règles de Yahtzee, je ne vois ici aucun argument convaincant pour justifier l'impossibilité de respecter rigoureusement le processus Red / Green / Refactor et d'obtenir un résultat correct.

Ce que la plupart des gens semblent oublier ici est la troisième étape du processus Red / Green / Refactor. D'abord, vous écrivez le test. Ensuite, vous écrivez la mise en œuvre la plus simple qui passe tous les tests. Ensuite, vous refactor.

C'est ici, dans ce troisième État, que vous pouvez mettre à profit toutes vos compétences professionnelles. C'est là que vous êtes autorisé à réfléchir sur le code.

Cependant, je pense que c’est une échappatoire de dire que vous devriez seulement "écrire la chose la plus simple possible qui ne soit pas complètement aveugle et qui, à l’évidence, soit incorrecte ". Si vous pensez en savoir assez sur la mise en œuvre, alors tout ce qui reste avant la solution complète sera manifestement incorrect . En ce qui concerne les conseils, ceci est donc plutôt inutile pour un novice.

Ce qui devrait vraiment arriver, c’est que si vous pouvez faire passer tous les tests avec une implémentation manifestement incorrecte , c’est un retour que vous devriez écrire un autre test .

Il est étonnant de constater combien de fois cela vous mène vers une implémentation totalement différente de celle que vous aviez en tête en premier. Parfois, l’alternative qui se développe de la sorte peut s’avérer meilleure que votre plan initial.

La rigueur est un outil d'apprentissage

Il est tout à fait logique de s'en tenir à des processus rigoureux comme Red / Green / Refactor tant que l'on apprend. Cela oblige l'apprenant à acquérir de l'expérience avec le TDD non seulement quand c'est facile, mais aussi quand c'est difficile.

Ce n'est que lorsque vous maîtriserez tous les aspects difficiles que vous serez en mesure de prendre une décision éclairée sur le moment de dévier du «vrai» chemin. C'est à ce moment que vous commencez à former votre propre chemin.

Mark Seemann
la source
«Pas un novice TDD ici, avec toutes les réticences habituelles à l’essayer. Intéressant à prendre si vous pouvez faire passer tous les tests avec une implémentation évidemment incorrecte, c’est le feedback que vous devriez écrire un autre test. Cela semble être un bon moyen de lutter contre la perception selon laquelle tester les implémentations "braindead" est une tâche inutile.
shambulator
1
Wow merci. Je suis vraiment effrayé par la tendance des gens à dire aux débutants en TDD (ou dans n'importe quelle discipline) de "ne vous inquiétez pas des règles, mais faites ce qui vous semble le mieux". Comment pouvez-vous savoir ce qui se sent le mieux quand vous n'avez aucune connaissance ou expérience? J'aimerais également mentionner le principe de priorité de transformation, ou ce code devrait devenir plus générique à mesure que les tests deviennent plus spécifiques. les partisans les plus inflexibles du TDD, comme oncle bob, ne souscrivaient pas à la notion "d'ajouter simplement une nouvelle déclaration if pour chaque test".
Sara
41

En tant que disclaimer, il s’agit de TDD tel que je le pratique et, comme le souligne judicieusement Kilian, je me méfierais de tous ceux qui suggèrent qu’il existe une bonne façon de le pratiquer. Mais peut-être que cela vous aidera ...

Tout d'abord, la chose la plus simple que vous puissiez faire pour réussir votre test serait la suivante:

public bool IsFullHouse(int roll1, int roll2, int roll3, int roll4, int roll5)
{
    return true;
}

C'est important parce que ce n'est pas à cause de certaines pratiques de TDD, mais parce que ce n'est pas vraiment une bonne idée de maîtriser parfaitement ces littéraux. L'une des choses les plus difficiles à comprendre avec TDD est que ce n'est pas une stratégie de test complète - c'est un moyen de se prémunir contre les régressions et de marquer les progrès tout en gardant le code simple. C'est une stratégie de développement et non une stratégie de test.

La raison pour laquelle je mentionne cette distinction est qu’elle aide à déterminer quels tests vous devriez écrire. La réponse à "quels tests devrais-je écrire?" est "quels que soient les tests dont vous avez besoin pour obtenir le code comme vous le souhaitez". Pensez à TDD comme un moyen de vous aider à comprendre les algorithmes et à raisonner au sujet de votre code. Donc, compte tenu de votre test et de mon implémentation "simple verte", quel test vient ensuite? Eh bien, vous avez établi quelque chose qui est un full, alors quand n'est-ce pas un full?

public void Returns_true_when_roll_is_full_house()
{
    FullHouseTester sut = new FullHouseTester();
    var actual = sut.IsFullHouse(1, 2, 3, 4, 5);

    Assert.IsFalse(actual);
}

Maintenant , vous devez trouver un moyen de faire la différence entre les deux cas de test qui est significatif . Personnellement, je voudrais ajouter quelques précisions à la question "faire la chose la plus simple pour réussir le test" et dire "faire la chose la plus simple pour réussir le test qui favorise votre mise en œuvre". La rédaction de tests échoués est votre prétexte pour modifier le code. Ainsi, lorsque vous écrivez chaque test, vous devez vous demander: "Qu'est-ce que mon code ne fait pas que je le souhaite et comment puis-je dénoncer cette lacune?" Cela peut également vous aider à rendre votre code robuste et à gérer les cas extrêmes. Que faites-vous si un appelant saisit un non-sens?

public void Returns_true_when_roll_is_full_house()
{
    FullHouseTester sut = new FullHouseTester();
    var actual = sut.IsFullHouse(-1, -2, -3, -4, -5);

    //I dunno - throw exception, return false, etc, whatever you think it should do....
}

En résumé, si vous testez toutes les combinaisons de valeurs, vous vous trompez certainement (et vous risquez de vous retrouver avec une explosion combinatoire de conditions). En ce qui concerne TDD, vous devez écrire le nombre minimum de tests élémentaires nécessaires pour obtenir l’algorithme souhaité. Tous les autres tests que vous écrirez commenceront en vert et deviendront ainsi de la documentation, en substance, et ne feront pas strictement partie du processus TDD. Vous ne rédigerez d'autres scénarios de test TDD que si les exigences changent ou si un bogue est exposé. Dans ce cas, vous documentez le problème avec un test, puis vous le faites passer.

Mise à jour:

J'ai commencé cela en tant que commentaire en réponse à votre mise à jour, mais cela a commencé à être assez long ...

Je dirais que le problème ne vient pas de l'existence de littéraux, point à la ligne, mais que la chose la plus simple est un conditionnel en 5 parties. Quand vous y réfléchissez, une condition en 5 parties est en fait assez compliquée. Il sera courant d’utiliser des littéraux lors de l’étape allant du rouge au vert puis de les résumer en constantes lors de l’étape de refactorisation ou de les généraliser lors d’un test ultérieur.

Au cours de mon propre voyage chez TDD, j'ai compris qu'il y avait une distinction importante à faire: il n'est pas bon de confondre "simple" et "obtus". C'est-à-dire que lorsque j'ai commencé, j'ai regardé les gens faire le TDD et je me suis dit: "ils font juste la chose la plus stupide possible pour faire passer les tests" et je l'ai imité pendant un moment, jusqu'à ce que je réalise que "simple" était subtilement différent que "obtus". Parfois, ils se chevauchent, mais souvent pas.

Donc, excuses si j'ai donné l'impression que le problème était l'existence de littéraux - ce n'est pas le cas. Je dirais que la complexité du conditionnel avec les 5 clauses est le problème. Votre premier rouge-à-vert peut simplement être "return true" parce que c'est vraiment simple (et obtus, par coïncidence). Le prochain cas de test, avec les (1, 2, 3, 4, 5) devra renvoyer faux, et c’est là que vous commencerez à sortir "obtus". Vous devez vous demander "pourquoi est (1, 1, 1, 2, 2) une salle comble et (1, 2, 3, 4, 5) ne l’est pas?" La chose la plus simple que vous puissiez trouver est peut-être que l’un a le dernier élément de séquence 5 ou le deuxième élément de séquence 2 et l’autre pas. Celles-ci sont simples, mais elles sont aussi (inutilement) obtuses. Ce que vous voulez vraiment faire, c'est "combien ont-ils du même nombre?" Donc, vous pouvez faire passer le deuxième test en vérifiant s'il y a une répétition. Dans l'un avec une répétition, vous avez une maison pleine, et l'autre pas. À présent, le test réussit et vous écrivez un autre scénario de test comportant une répétition, mais qui n'est pas complet, pour affiner davantage votre algorithme.

Vous pouvez ou non faire cela avec les littéraux au fur et à mesure, et c'est bien si vous le faites. Mais l'idée générale consiste à développer votre algorithme de manière «organique» à mesure que vous ajoutez de nouveaux cas.

Erik Dietrich
la source
J'ai mis à jour ma question pour ajouter quelques informations supplémentaires sur les raisons pour lesquelles j'ai commencé avec l'approche littérale.
Kristof Claes
9
C'est une excellente réponse.
Tallseth
1
Merci beaucoup pour votre réponse réfléchie et bien expliquée. En fait, cela a beaucoup de sens maintenant que j'y réfléchis.
Kristof Claes
1
Des tests approfondis ne signifient pas tester toutes les combinaisons ... C'est idiot. Dans ce cas particulier, prenons un full ou deux et un couple de full. Également toutes les combinaisons spéciales qui pourraient causer des problèmes (par exemple, 5 d'un genre).
Schleis
3
+1 Les principes qui sous-tendent cette réponse sont décrits dans la Transformation Priority Premise
Mark Seemann
5

Tester cinq valeurs littérales particulières dans une combinaison donnée n’est pas la méthode la plus «simple» pour mon cerveau fébrile. Si la solution à un problème est vraiment évidente (comptez si vous en avez exactement trois et deux d’ une valeur quelconque ), alors allez-y, codez cette solution et écrivez des tests qu'il serait très peu probable de satisfaire accidentellement avec la quantité de code que vous avez écrit (c.-à-d. différents littéraux et différents ordres des triples et des doubles).

Les maximes TDD sont vraiment des outils, pas des croyances religieuses. Leur but est de vous amener à écrire rapidement du code correct, bien factorisé. Si une maxime fait évidemment obstacle à cette évolution, passez à l'étape suivante. Votre projet contiendra beaucoup d'éléments non évidents où vous pourrez l'appliquer.

Kilian Foth
la source
5

La réponse d'Erik est excellente, mais je pensais pouvoir partager un truc en rédaction de test.

Commencez avec ce test:

[Test]
public void FullHouseReturnsTrue()
{
    var pairNum = AnyDiceValue();
    var trioNum = AnyDiceValue();

    Assert.That(sut.IsFullHouse(trioNum, pairNum, trioNum, pairNum, trioNum));
}

Ce test est encore meilleur si vous créez une Rollclasse au lieu de passer 5 paramètres:

[Test]
public void FullHouseReturnsTrue()
{
    var roll = AnyFullHouse();

    Assert.That(sut.IsFullHouse(roll));
}

Cela donne cette implémentation:

public bool IsFullHouse(Roll toCheck)
{
    return true;
}

Puis écris ce test:

[Test]
public void StraightReturnsFalse()
{
    var roll = AnyStraight();

    Assert.That(sut.IsFullHouse(roll), Is.False);
}

Une fois que cela passe, écrivez celui-ci:

[Test]
public void ThreeOfAKindReturnsFalse()
{
    var roll = AnyStraight();

    Assert.That(sut.IsFullHouse(roll), Is.False);
}

Après cela, je parie que vous n’avez plus besoin d’écrire (peut-être deux paires, ou peut-être yahtzee, si vous pensez que ce n’est pas une salle comble).

Évidemment, implémentez vos méthodes pour renvoyer des jets aléatoires qui répondent à vos critères.

Cette approche présente quelques avantages:

  • Vous n'avez pas besoin de passer un test dont le seul but est de vous empêcher de rester bloqué sur des valeurs spécifiques
  • Les tests communiquent très bien votre intention (le code du premier test crie "toute la maison est vraie")
  • cela vous amène rapidement au point de travailler sur la viande du problème
  • Parfois, il remarquera des cas que vous n'avez pas pensé
tallseth
la source
Si vous utilisez cette approche, vous devrez améliorer vos messages de journal dans vos instructions Assert.That. Le développeur doit voir quelle entrée a provoqué l'échec.
Bringer128
Cela ne crée-t-il pas un dilemme poulet ou œuf? Lorsque vous implémentez AnyFullHouse (en utilisant également TDD), n’avez-vous pas besoin d’IsFullHouse pour vérifier son exactitude? Spécifiquement, si AnyFullHouse a un bogue, ce bogue pourrait être répliqué dans IsFullHouse.
waxwing
AnyFullHouse () est une méthode dans un scénario de test. Avez-vous typiquement TDD vos cas de test? En outre, il est beaucoup plus simple de créer un exemple aléatoire de full house (ou de tout autre rôle) que de tester son existence. Bien sûr, si votre test contient un bogue, il peut être répliqué dans le code de production. C’est vrai pour tous les tests.
Tallseth
AnyFullHouse est une méthode "helper" dans un scénario de test. Si elles sont assez générales, les méthodes d'assistance sont également testées!
Mark Hurd
Devrait IsFullHousevraiment revenir truesi pairNum == trioNum ?
recursion.ninja
2

Je peux penser à deux manières principales que je considérerais en testant ceci;

  1. Ajoutez "certains" autres cas de test (~ 5) d'ensembles valables, et le même nombre de faux attendus ({1, 1, 2, 3, 3} est un bon exemple. Rappelez-vous que 5 d'entre eux pourraient être reconnu comme "3 du même plus une paire" par une implémentation incorrecte). Cette méthode suppose que le développeur n'essaie pas seulement de réussir les tests, mais qu'il l'implémente correctement.

  2. Testez tous les jeux de dés possibles (il n'y a que 252 différents). Bien entendu, cela suppose que vous ayez un moyen de savoir quelle est la réponse attendue (cela s'appelle un oracle.). Cela pourrait être une implémentation de référence de la même fonction, ou un être humain. Si vous voulez être vraiment rigoureux, cela pourrait valoir la peine de coder manuellement chaque résultat attendu.

Il se trouve que j’ai écrit une fois une AI de Yahtzee, qui devait bien sûr connaître les règles. Vous trouverez le code de la partie relative à l'évaluation du score ici . Notez que l'implémentation concerne la version scandinave (Yatzy) et que notre implémentation suppose que les dés sont classés dans l'ordre.

ansjob
la source
La question à un million de dollars est la suivante: avez-vous dérivé l'IA Yahtzee en utilisant du TDD pur? Mon pari est que vous ne pouvez pas; vous devez utiliser la connaissance du domaine, qui, par définition, n'est pas aveugle :)
Andres F.
Oui, je suppose que tu as raison. Il s'agit d'un problème général avec TDD, à savoir que les scénarios de test nécessitent des sorties attendues, sauf si vous souhaitez uniquement tester les plantages inattendus et les exceptions non gérées.
ansjob
0

Cet exemple manque vraiment le point. Nous parlons ici d'une fonction simple, pas d'une conception de logiciel. Est-ce un peu compliqué? oui, alors vous le décomposez. Et vous ne testez absolument pas toutes les entrées possibles de 1, 1, 1, 1, 1 à 6, 6, 6, 6, 6, 6. La fonction en question ne nécessite pas d'ordre, mais une combinaison, à savoir AAABB.

Vous n'avez pas besoin de 200 tests de logique distincts. Vous pouvez utiliser un ensemble par exemple. Presque tous les langages de programmation en ont un:

Set set;
set.add(a);
set.add(b);
set.add(c);
set.add(d);
set.add(e);

if(set.size() == 2) { // means we *must* be of the form AAAAB or AAABB.
    if(a==b==c==d) // eliminate AAAAB
        return false;
    else
        return true;
}
return false;

Et si vous obtenez une entrée qui n'est pas un résultat valable pour Yahtzee, vous devriez lancer comme s'il n'y avait pas de lendemain.

Jay Mueller
la source