Méthode de test unitaire qui renvoie une collection tout en évitant la logique dans le test

14

Je teste une méthode qui consiste à générer une collection d'objets de données. Je veux vérifier que les propriétés des objets sont définies correctement. Certaines propriétés seront définies sur la même chose; d'autres seront définis sur une valeur qui dépend de leur position dans la collection. La façon naturelle de le faire semble être une boucle. Cependant, Roy Osherove déconseille fortement d'utiliser la logique dans les tests unitaires ( Art of Unit Testing , 178). Il dit:

Un test qui contient de la logique teste généralement plus d'une chose à la fois, ce qui n'est pas recommandé, car le test est moins lisible et plus fragile. Mais la logique de test ajoute également de la complexité qui peut contenir un bogue caché.

Les tests doivent, en règle générale, être une série d'appels de méthode sans flux de contrôle, pas même try-catch, et avec des appels d'assertion.

Cependant, je ne vois rien de mal dans ma conception (comment générer autrement une liste d'objets de données, dont certaines valeurs dépendent de leur emplacement dans la séquence? - je ne peux pas exactement les générer et les tester séparément). Y a-t-il quelque chose qui ne respecte pas les tests dans ma conception? Ou suis-je trop attaché à l'enseignement d'Osherove? Ou y a-t-il une magie secrète de test unitaire que je ne connais pas qui contourne ce problème? (J'écris en C # / VS2010 / NUnit, mais je recherche des réponses indépendantes du langage si possible.)

Kazark
la source
4
Je recommande de ne pas boucler. Si votre test est que la troisième chose a sa barre définie sur Frob, alors écrivez un test pour vérifier spécifiquement que la barre de la troisième chose est Frob. C'est un test en soi, allez-y directement, pas de boucle. Si votre test est que vous obtenez une collection de 5 choses, c'est aussi un test. Cela ne veut pas dire que vous n'avez jamais de boucle (explicite ou autre), c'est juste que vous n'en avez pas souvent besoin. Aussi, traitez le livre d'Osherove comme plus de directives que de règles réelles.
Anthony Pegram
1
Les ensembles @AnthonyPegram ne sont pas ordonnés - Frob peut parfois être 3e, parfois 2e. Vous ne pouvez pas vous y fier, rendant une boucle (ou une fonctionnalité de langage comme Python in) nécessaire, si le test est "Frob a été ajouté avec succès à une collection existante".
Izkata
1
@Izbata, sa question mentionne spécifiquement que la commande est importante. Ses mots: "d'autres seront mis à une valeur qui dépend de leur position dans la collection." Il existe de nombreux types de collections en C # (le langage qu'il référence) qui sont ordonnées par insertion. D'ailleurs, vous pouvez également compter sur l'ordre avec des listes en Python, un langage que vous mentionnez.
Anthony Pegram
Supposons également que vous testiez une méthode Reset sur une collection. Vous devez parcourir la collection et vérifier chaque élément. Selon la taille de la collection, ne pas la tester en boucle est ridicule. Ou disons que je teste quelque chose qui est censé incrémenter chaque élément d'une collection. Vous pouvez définir tous les éléments sur la même valeur, appeler votre incrément, puis vérifier. Ce test est nul. Vous devez définir plusieurs d'entre eux sur des valeurs différentes, appeler incrémenter et vérifier que toutes les différentes valeurs ont été incrémentées correctement. La vérification d'un seul élément au hasard dans la collection laisse beaucoup au hasard.
iheanyi
Je ne vais pas répondre de cette façon, car j'obtiendrai des millions de votes négatifs, mais je me contente souvent de toString()la collection et de la comparer à ce qu'elle devrait être. Simple et fonctionne.
user949300

Réponses:

16

TL; DR:

  • Écrivez le test
  • Si le test en fait trop, le code peut aussi en faire trop.
  • Ce n'est peut-être pas un test unitaire (mais pas un mauvais test).

La première chose à tester est que le dogme est inutile. J'aime lire The Way of Testivus qui souligne certains problèmes avec le dogme d'une manière légère.

Écrivez le test qui doit être écrit.

Si le test doit être écrit d'une manière ou d'une autre, écrivez-le de cette façon. Tenter de forcer le test dans une disposition de test idéalisée ou de ne pas l'avoir du tout n'est pas une bonne chose. Avoir un test aujourd'hui qui teste c'est mieux que d'avoir un test "parfait" un jour plus tard.

Je vais également souligner le bit sur le test laid:

Lorsque le code est moche, les tests peuvent être moche.

Vous n'aimez pas écrire des tests laids, mais le code laid doit être testé le plus.

Ne laissez pas le code laid vous empêcher d'écrire des tests, mais laissez le code laid vous empêcher d'en écrire plus.

Ceux-ci peuvent être considérés comme des truismes pour ceux qui suivent depuis longtemps ... et ils deviennent juste enracinés dans la manière de penser et d'écrire des tests. Pour les personnes qui ne l'ont pas été et tentent d'en arriver là, les rappels peuvent être utiles (je trouve même que les relire m'aide à éviter de m'enfermer dans un dogme).


Considérez cela lors de l'écriture d'un test moche, si le code peut être une indication que le code essaie de faire trop trop. Si le code que vous testez est trop complexe pour être correctement exercé en écrivant un test simple, vous pouvez envisager de diviser le code en parties plus petites qui peuvent être testées avec les tests les plus simples. Il ne faut pas écrire un test unitaire qui fait tout (ce n'est peut-être pas un test unitaire alors). Tout comme les «objets divins» sont mauvais, les «tests unitaires divins» sont également mauvais et devraient être des indications pour revenir en arrière et revoir le code.

Vous devriez pouvoir exercer tout le code avec une couverture raisonnable grâce à des tests aussi simples. Les tests qui effectuent davantage de tests de bout en bout qui traitent de questions plus importantes («J'ai cet objet, rassemblé en XML, envoyé au service Web, via les règles, annulé et non corrigé») est un excellent test - mais il ne l'est certainement pas. 't un test unitaire (et tombe dans le domaine des tests d'intégration - même s'il a des services moqués qu'il appelle et personnalisé dans les bases de données de mémoire pour faire le test). Il peut toujours utiliser le framework XUnit pour les tests, mais le framework de tests n'en fait pas un test unitaire.


la source
7

J'ajoute une nouvelle réponse parce que mon point de vue est différent de lorsque j'ai écrit la question et la réponse d'origine; il n'est pas logique de les mailler ensemble en un seul.

J'ai dit dans la question d'origine

Cependant, je ne vois rien de mal dans ma conception (comment générer autrement une liste d'objets de données, dont certaines valeurs dépendent de l'endroit où ils se trouvent dans la séquence? - je ne peux pas exactement les générer et les tester séparément)

C'est là que je me suis trompé. Après avoir fait de la programmation fonctionnelle au cours de la dernière année, je me rends compte maintenant que j'avais juste besoin d'une opération de collecte avec un accumulateur. Ensuite, je pouvais écrire ma fonction comme une fonction pure qui fonctionnait sur une chose et utiliser une fonction de bibliothèque standard pour l'appliquer à la collection.

Donc ma nouvelle réponse est: utilisez des techniques de programmation fonctionnelles et vous éviterez ce problème entièrement la plupart du temps. Vous pouvez écrire vos fonctions pour opérer sur des choses uniques et les appliquer uniquement à des collections de choses au dernier moment. Mais s'ils sont purs, vous pouvez les tester sans référence aux collections.

Pour une logique plus complexe, s'appuyer sur des tests basés sur les propriétés . Lorsqu'ils ont une logique, celle-ci doit être inférieure et inverse à la logique du code testé, et chaque test vérifie tellement plus qu'un test unitaire basé sur le cas que la petite quantité de logique en vaut la peine.

Surtout s'appuyer toujours sur vos types . Obtenez les types les plus forts que vous pouvez et utilisez-les à votre avantage. Cela réduira le nombre de tests que vous devez écrire en premier lieu.

Kazark
la source
4

N'essayez pas de tester trop de choses à la fois. Chacune des propriétés de chaque objet de données dans la collection est trop pour un test. Au lieu de cela, je recommande:

  1. Si la collection est de longueur fixe, écrivez un test unitaire pour valider la longueur. S'il s'agit d'une longueur variable, écrivez plusieurs tests pour les longueurs qui caractériseront son comportement (par exemple 0, 1, 3, 10). Dans tous les cas, ne validez pas les propriétés dans ces tests.
  2. Écrivez un test unitaire pour valider chacune des propriétés. Si la collection est de longueur fixe et courte, assurez-vous simplement d'une propriété de chacun des éléments pour chaque test. S'il est de longueur fixe mais long, choisissez un échantillon représentatif mais petit des éléments à faire valoir contre une propriété chacun. S'il est de longueur variable, générez une collection relativement courte mais représentative (c.-à-d. Peut-être trois éléments) et affirmez contre une propriété de chacun.

Le faire de cette façon rend les tests suffisamment petits pour que laisser de côté les boucles ne semble pas douloureux. Exemple C # / unité, méthode donnée en cours de test ICollection<Foo> generateFoos(uint numberOfFoos):

[Test]
void generate_zero_foos_returns_empty_list() { ... }
void generate_one_foo_returns_list_of_one_foo() { ... }
void generate_three_foos_returns_list_of_three_foos() { ... }
void generated_foos_have_sequential_ID()
{
    var foos = generateFoos(3).GetEnumerable();
    foos.MoveNext();
    Assert.AreEqual("ID1", foos.Current.id);
    foos.MoveNext();
    Assert.AreEqual("ID2", foos.Current.id);
    foos.MoveNext();
    Assert.AreEqual("ID3", foos.Current.id);
}
void generated_foos_have_bar()
{
    var foos = generateFoos(3).GetEnumerable();
    foos.MoveNext();
    Assert.AreEqual("baz", foos.Current.bar);
    foos.MoveNext();
    Assert.AreEqual("baz", foos.Current.bar);
    foos.MoveNext();
    Assert.AreEqual("baz", foos.Current.bar);
}

Si vous êtes habitué au paradigme du «test unitaire plat» (pas de structures / logique imbriquées), ces tests semblent assez propres. Ainsi, la logique est évitée dans les tests en identifiant le problème d'origine comme essayant de tester trop de propriétés à la fois, plutôt que de manquer de boucles.

Kazark
la source
1
Osherove aurait la tête sur un plateau pour avoir 3 assertions. ;) Le premier échec signifie que vous ne validez jamais le reste. Notez également que vous n'avez pas vraiment évité la boucle. Vous venez de le développer explicitement dans sa forme exécutée. Pas une critique sévère, mais juste une suggestion pour obtenir un peu plus de pratique en isolant vos cas de test au minimum possible, pour vous donner des commentaires plus spécifiques quand quelque chose échoue, tout en continuant à valider d'autres cas qui pourraient éventuellement réussir (ou échouer, avec leurs propres commentaires spécifiques).
Anthony Pegram
3
@AnthonyPegram Je connais le paradigme d'un assert par test. Je préfère le mantra "tester une chose" (comme le préconise Bob Martin, contre une assertion par test, dans Clean Code ). Note latérale: les cadres de tests unitaires qui ont "s'attendre" à "affirmer" sont agréables (Google Tests). Pour le reste, pourquoi ne divisez-vous pas vos suggestions en une réponse complète, avec des exemples? Je pense que je pourrais en bénéficier.
Kazark