Existe-t-il une meilleure façon d'écrire des tests unitaires qu'une série d '«AssertEquals»?

12

Voici un exemple de base de ce que doit être mon test unitaire, en utilisant qunit:

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Insert title here</title>

<link rel="stylesheet" href="qunit/qunit-1.13.0.css">
<script src = "qunit/qunit-1.13.0.js"></script>
<script src = "../js/fuzzQuery.js"></script>

<script>

test("Fuzz Query Basics", function()
        {
            equal(fuzzQuery("name:(John Smith)"), "name:(John~ Smith~)");
            equal(fuzzQuery("name:Jon~0.1"), "name:Jon~0.1");
            equal(fuzzQuery("Jon"), "Jon~");
            //etc

        }
    );

</script>
</head>
<body>
    <div id="qunit"></div>
</body>
</html>

Maintenant, je pensais que c'était un peu répétitif.

Pourrait mettre toutes les entrées / sorties dans un tableau et les parcourir.

test("Fuzz Query Basics", function()
        {
            var equals = [
                           ["name:(John Smith)", "name:(John~ Smith~)"],
                           ["name:Jon~0.1", "name:Jon~0.1"],
                           ["Jon", "Jon~"]
                           ];

            for (var i = 0; i<equals.length; i++)
                {
                    equal(fuzzQuery(equals[i][0]), equals[i][1]);               
                }

        }
    );

Et cela fonctionne bien.

Le seul avantage auquel je peux penser pour cette deuxième méthode, c'est que s'il s'avère que vous ne voulez pas vraiment l'utiliser, equalil est plus facile de faire ce changement en un seul endroit.

En termes de lisibilité, je ne pense pas que ce soit concluant dans les deux cas, même si je préfère probablement la seconde.

En le résumant davantage, vous pouvez placer les cas d'entrée / sortie dans un fichier CSV séparé, ce qui pourrait le rendre plus facile à modifier.

La question est - quelles sont les conventions générales concernant l'écriture de ces types de tests unitaires?

Y a-t-il une raison pour laquelle vous ne devriez pas les mettre en tableaux?

dwjohnston
la source
Est-ce que l'un ou l'autre vous indiquera quelle valeur a échoué?
JeffO
1
@JeffO - Oui - Avec QUnit - Si un test échoue, la sortie affichera la valeur attendue et la valeur réelle.
dwjohnston

Réponses:

8

Vos tests refactorisés ont une odeur: logique de test conditionnelle .

Les raisons pour lesquelles vous devez éviter d'écrire une logique conditionnelle dans vos tests sont doubles. Le premier est qu'il sape votre capacité à être sûr que votre code de test est correct, comme décrit dans l'article lié des modèles xUnit.

La seconde est qu'elle obscurcit la signification des tests. Nous écrivons des méthodes de test parce qu'elles mettent la logique pour tester un comportement donné en un seul endroit, et nous permettent de lui donner un nom descriptif (voir l'article BDD original de Dan North pour une exploration de la valeur des bons noms pour les tests). Lorsque vos tests sont cachés dans une seule fonction avec une forboucle, cela masque la signification du code pour le lecteur. Non seulement le lecteur doit comprendre la boucle, mais il doit également démêler mentalement tous les différents comportements testés dans la boucle.

La solution, comme toujours, est de monter d'un niveau d'abstraction. Utilisez un cadre de test qui vous donne des tests paramétrés , comme le font xUnit.NET ou Contexts (avertissement: j'ai écrit Contexts). Cela vous permet de regrouper les tests de triangulation pour le même comportement de manière naturelle, tout en séparant les tests des comportements séparés.

Benjamin Hodgson
la source
Au fait, bonne question
Benjamin Hodgson
1
1) Si vous montez d'un niveau d'abstraction, ne cachez-vous pas ces mêmes détails qui, selon vous, sont masqués par la boucle for? 2) Je ne sais pas si les tests paramétrés sont applicables ici. Il semble qu'il y ait des parallèles ici quelque part, mais j'ai eu beaucoup de situations similaires aux PO où j'avais un ensemble de données de 10 à 20 valeurs et je voulais juste les exécuter toutes via SUT. Oui, chaque valeur est différente et teste potentiellement des bouddhistes différents, mais semble réellement venir "inventer" des noms de test pour chaque valeur unique serait une exagération. J'ai trouvé un rapport valeur / code optimal en utilisant des ...
DXM
... boucles. Tant que lorsque le test échoue, l'assertion imprime exactement ce qui a échoué, le développeur a suffisamment de commentaires pour localiser avec précision le problème.
DXM
@DXM 1) le framework de test fournit la fonctionnalité de test paramétrée. Nous faisons implicitement confiance au framework de test afin de ne pas écrire de test pour lui. 2) les tests paramétrés sont exactement dans ce but: vous effectuez exactement les mêmes étapes à chaque fois mais avec des valeurs d'entrée / sortie différentes. Le framework de test vous évite d'avoir à écrire des noms pour chacun en exécutant les différentes entrées via la même méthode de test.
Benjamin Hodgson
5

Il semble que vous souhaitiez vraiment un test unitaire basé sur les données. Puisque vous avez mentionné l'utilisation de QUnit, j'ai trouvé un plugin qui permet des tests paramétrés:

https://github.com/AStepaniuk/qunit-parameterize

Il n'y a rien de mal idéologiquement avec un test basé sur les données, tant que le code de test lui-même n'est pas conditionnel. En regardant votre code de test, il semble être un très bon candidat pour un test basé sur les données.

Exemple de code pour le fichier LISEZMOI GitHub:

QUnit
    .cases([
        { a : 2, b : 2, expectedSum : 4 },
        { a : 5, b : 5, expectedSum : 10 },
        { a : 40, b : 2, expectedSum : 42 }
    ])
    .test("Sum test", function(params) {
        var actualSum = sum(params.a, params.b);
        equal(actualSum, params.expectedSum);
    });
Greg Burghardt
la source
1
D'accord, cela ressemble à un test basé sur les données. Mais il semble que c'est ce qu'il a déjà dans son deuxième exemple de code.
Robert Harvey
1
@RobertHarvey - Correct. Il y a un terme accepté pour ce qu'il essaie d'accomplir, et un plugin existe pour le framework de test utilisé pour faciliter l'écriture de ces types de tests. J'ai pensé qu'il valait la peine de noter dans une réponse pour l'avenir, c'est tout.
Greg Burghardt
1

Vous vous répétez moins en utilisant le tableau qui est plus facile à gérer. Une approche que j'aime utiliser est d'avoir une méthode distincte qui organise, agit et affirme les tests, mais accepte les paramètres d'entrée avec lesquels je teste donc j'ai 1 méthode de test par ensemble d'entrées.

Cela me permet de dire instantanément quels tests / entrées échouent.

Kevin
la source
0

J'aime votre deuxième approche, mais j'ajouterais 2 points

  • n'utilisez pas de tableaux pour stocker des données testées, car travailler avec des index n'est pas une manière propre
  • n'utilisez pas de forboucles

"

[
    {
        process: "name:(John Smith)",
        result: "name:(John~ Smith~)"
    },
    {
        process: "name:Jon~0.1", 
        result: "name:Jon~0.1"
    },
    {
        process: "Jon", 
        result: "Jon~"
    }
]
.forEach(function(data){

    var result = fuzzQuery(data.process);
    equal(result, data.result);
});

Je ne suis pas sûr de qunit, mais un bon testeur vous montrera quelle chaîne d'entrée a échoué et quel était le résultat attendu

tenbits
la source