it-swarm-fr.com

Écrire le code minimum pour passer un test d'unité - sans tricher!

Lorsque vous faites TDD et écrivez un test unitaire, comment résiste-t-on l'envie de "tricher" lors de la rédaction de la première itération du code "Mise en œuvre" que vous testez?

Par exemple:
[.____], Jetons-je besoin de calculer la factorielle d'un nombre. Je commence par un test unitaire (utilisant Mstest) quelque chose comme:

[TestClass]
public class CalculateFactorialTests
{
    [TestMethod]
    public void CalculateFactorial_5_input_returns_120()
    {
        // Arrange
        var myMath = new MyMath();
        // Act
        long output = myMath.CalculateFactorial(5);
        // Assert
        Assert.AreEqual(120, output);
    }
}

J'exécute ce code et il échoue depuis la méthode CalculateFactorial _ n'existe même pas. Donc, j'écris maintenant la première itération du code pour implémenter la méthode sous test, écrire le minimumcode requis pour réussir le test.

La chose est que je suis continuellement tenté d'écrire ce qui suit:

public class MyMath
{
    public long CalculateFactorial(long input)
    {
        return 120;
    }
}

Ceci est, techniquement, correct dans ce vraiment est le code minimum requis pour Faites ce passage de test spécifique (go vert), bien que ce soit clairement une "triche "Puisque ce n'est vraiment pas tentative Pour effectuer la fonction de calculer une factorielle. Bien entendu, la partie refactory devient maintenant un exercice dans "Écrire la fonctionnalité correcte" plutôt qu'un véritable refactoring de la mise en œuvre. De toute évidence, l'ajout de tests supplémentaires avec différents paramètres échouera et obligera un refactoring, mais vous devez commencer par un test.

Donc, ma question est de savoir comment obtenez-vous cet équilibre entre "Écrire le code minimal pour réussir le test" tout en le gardant fonctionnel et dans l'esprit de ce que vous essayez réellement de réaliser?

37
CraigTP

C'est parfaitement légitime. Rouge, vert, refacteur.

Le premier test passe.

Ajoutez le deuxième test, avec une nouvelle entrée.

Maintenant, passez rapidement au vert, vous pourriez ajouter un if-sinon, qui fonctionne bien. Il passe, mais vous n'êtes pas encore fait.

La troisième partie du refacteur rouge, vert, est la plus importante. refacteur pour supprimer la duplication. Vous aurez une duplication dans votre code maintenant. Deux déclarations renvoyant des entiers. et le seul moyen d'éliminer cette duplication consiste à coder la fonction correctement.

Je ne dis pas, je ne l'écris pas correctement la première fois. Je dis juste que ça ne triche pas si tu ne le fais pas.

46
CaffGeek

Il est clair que la compréhension de l'objectif ultime et la réalisation d'un algorithme qui répond à cet objectif est nécessaire.

TDD n'est pas une balle magique pour la conception; Vous devez toujours savoir comment résoudre des problèmes à l'aide du code et vous devez toujours savoir comment faire cela à un niveau supérieur à quelques lignes de code pour effectuer une passe de test.

J'aime l'idée de TDD parce qu'il encourage un bon design; Cela vous fait penser à la manière dont vous pouvez écrire votre code afin qu'il soit essentiel et que la philosophie appuie généralement le code vers une meilleure conception dans son ensemble. Mais vous devez toujours savoir comment archiver une solution.

Je ne préfère pas les philosophies de TDD réductionnistes qui affirment que vous pouvez développer une demande en écrivant simplement la plus petite quantité de code pour passer un test. Sans réfléchir à l'architecture, , cela ne fonctionnera pas et votre exemple prouve que.

Oncle Bob Martin dit ceci :

Si vous ne faites pas de développement testé, il est très difficile de vous appeler un professionnel. Jim Coollin m'a appelé sur le tapis pour celui-ci. Il n'aimait pas que je disais ça. En fait, sa position actuelle est que le développement axé sur les tests est de la destruction des architectures, car les gens écrivent des tests à l'abandon de tout autre type de pensée et déchirant leurs architectes à l'écart de la ruée vers la ruée vers le passage et il a un point intéressant, C'est un moyen intéressant d'abuser du rituel et de perdre l'intention de la discipline.

si vous ne pensez pas à l'architecture, si ce que vous faites à la place, ignorez-vous à l'architecture et de lancer des tests ensemble et de les faire passer, vous détruisez la chose qui permettra à l'immeuble de rester en place parce que c'est la concentration sur le Structure du système et des décisions de conception solides qui aident le système à maintenir son intégrité structurelle.

Vous ne pouvez pas simplement jeter un tas de tests ensemble et les faire passer pendant une décennie après une décennie après une décennie et supposer que votre système va survivre. Nous ne voulons pas nous évoluer en enfer. Donc, un bon développeur axé sur les tests est toujours conscient de faire des décisions architecturales, de penser toujours à la grande image.

25
Robert Harvey

Lorsque vous n'avez écrit qu'un seul test unitaire, la mise en œuvre d'une ligne (return 120;) est légitime. Écrire une boucle calculant la valeur de 120 - qui tricherait!

De tels tests initiaux simples sont un bon moyen d'attraper des cas de bord et d'empêcher les erreurs uniques. Cinq en réalité, ce n'est pas la valeur d'entrée que je commencerais.

Une règle générale qui pourrait être utile ici est la suivante: zéro, un, beaucoup, beaucoup. Zéro et un sont des cas de bord importants pour la factorielle. Ils peuvent être mis en œuvre avec des doublures. Le "nombreux" cas de test (par exemple 5!) Vous obligerait alors à écrire une boucle. Le cas de test "lots" (1000 !?) pourrait vous forcer à mettre en place un algorithme alternatif pour gérer de très grands nombres.

10
azheglov

Tant que vous n'avez qu'un seul test, le code minimal nécessaire pour passer le test est vraiment return 120;, et vous pouvez facilement le garder pour cela tant que vous n'avez plus de tests.

Cela vous permet de reporter d'autres conceptions jusqu'à ce que vous écriviez les tests qui exercent d'autres valeurs de retour de cette méthode.

N'oubliez pas que le test est la version annoncable de votre spécification, et si tout que Spécification indique que f (6) = 120 puis qui correspond parfaitement à la facture.

5
user1249

Si vous êtes capable de "tricher" de telle manière, cela suggère que vos tests d'unités sont défectueux.

Plutôt que de tester la méthode factorielle avec une seule valeur, testez c'était une plage de valeurs. Les tests axés sur les données peuvent aider ici.

Affichez vos tests unitaires comme une manifestation des exigences - elles doivent définir collectivement le comportement de la méthode qu'ils testent. (Ceci est connu sous le nom de Développement dirigé par le comportement - c'est l'avenir ;-))

Alors demandez-vous - si quelqu'un devait changer la mise en œuvre en quelque chose de mal incorrect, vos tests passaient-ils toujours ou diraient-ils "accrocher une minute!"?

Portant cela à l'esprit, si votre seul test était celui de votre question, alors techniquement, la mise en œuvre correspondante est correcte. Le problème est ensuite considéré comme des exigences mal définies.

4
Nobody

Écrire des tests "triche" est correct, pour des valeurs suffisamment petites de "OK". Mais Rappel - Le test de l'unité n'est complet que lorsque tous les tests passent et aucun nouvel essai ne peut être écrit qui échouera . Si vous voulez vraiment avoir une méthode calculée contenant un tas de si déclarations (ou même mieux, une instruction Big commutateur/case :-) Vous pouvez le faire, et Puisque vous traitez d'un numéro de précision fixe, le code requis pour la mettre en œuvre est fini (bien que probablement plutôt grand et laid, et peut-être limité par le compilateur ou les limitations du système sur la taille maximale du code de la procédure). À ce stade si vous vraiment insister sur le fait que tout le développement doit être conduit par un test d'unité, vous pouvez écrire un test qui oblige le code à calculer le résultat une quantité de temps plus courte que celle qui peut être accomplie en suivant toutes les branches de la déclaration si.

Fondamentalement, TDD peut vous aider à écrire du code qui implémente les exigences correctement, mais cela ne peut pas vous forcer à écrire bon code. C'est à toi de voir.

Partager et profiter.

Il suffit d'écrire plus de tests. Finalement, ce serait plus court d'écrire

public long CalculateFactorial(long input)
{
    return input <= 1 ? 1 : CalculateFactorial(input-1)*input;
}

que

public long CalculateFactorial(long input)
{
    switch (input) {
       case 0: return 1;
       case 1: return 1;
       case 2: return 2;
       case 3: return 6;
       case 4: return 24;
       case 5: return 120;
    }
}

:-)

3
P Shved

Je suis d'accord à 100% avec la suggestion de Robert Harveys ici, il ne s'agit pas seulement de faire passer des tests, vous devez également garder l'objectif général à l'esprit.

En tant que solution à votre point de vue de "Il est seulement vérifié de travailler avec un ensemble donné d'entrées", je proposerais d'utiliser des tests axés sur les données, tels que la théorie de Xunit. Le pouvoir derrière ce concept est qu'il vous permet de créer facilement des spécifications d'entrées aux sorties.

Pour les facteurs facteurs, un test ressemblerait à ceci:

    [Theory]
    [InlineData(0, 1)]
    [InlineData( 1, 1 )]
    [InlineData( 2, 2 )]
    [InlineData( 3, 6 )]
    [InlineData( 4, 24 )]
    public void Test_Factorial(int input, int expected)
    {
        int result = Factorial( input );
        Assert.Equal( result, expected);
    }

Vous pouvez même mettre en œuvre une offre de données de test (qui retourne IEnumerable<Tuple<xxx>>) et encoder un invariant mathématique, tel que de la division répétée par N donnera N-1).

Je trouve que ce TP soit un moyen de tester très puissant.

1
Johannes Rudolph

Si vous êtes toujours capable de tricher, les tests ne suffisent pas. Écrivez plus de tests! Pour votre exemple, je vais essayer d'ajouter des tests avec l'entrée 1, -1, -1000, 0, 10, 200.

Néanmoins, si vous vous engagez vraiment à tromper, vous pouvez écrire une infinie si, alors. Dans ce cas, rien ne pourrait aider à l'exception de la révision du code. Vous seriez bientôt pris sur le test d'acceptation (écrit par une autre personne!)

Le problème des tests unitaires est parfois des programmeurs les examinent comme un travail inutile. La bonne façon de les voir est l'outil que vous pourrez faire le résultat de votre travail correct. Donc, si vous créez un si, alors, vous savez inconsciemment qu'il existe d'autres cas à considérer. Cela signifie que vous devez écrire un autre test. Et ainsi de suite jusqu'à ce que vous réalisiez que la triche ne fonctionne pas et qu'il est préférable de simplement coder la bonne voie. Si vous pensez toujours que vous n'êtes pas fini, vous n'êtes pas fini.

1
nanda