Comment tester lorsque l'organisation des données est trop lourde?

19

J'écris un analyseur et dans le cadre de cela, j'ai une Expanderclasse qui "développe" une seule instruction complexe en plusieurs instructions simples. Par exemple, cela développerait ceci:

x = 2 + 3 * a

dans:

tmp1 = 3 * a
x = 2 + tmp1

Maintenant, je réfléchis à la façon de tester cette classe, en particulier comment organiser les tests. Je pouvais créer manuellement l'arbre de syntaxe d'entrée:

var input = new AssignStatement(
    new Variable("x"),
    new BinaryExpression(
        new Constant(2),
        BinaryOperator.Plus,
        new BinaryExpression(new Constant(3), BinaryOperator.Multiply, new Variable("a"))));

Ou je pourrais l'écrire sous forme de chaîne et l'analyser:

var input = new Parser().ParseStatement("x = 2 + 3 * a");

La deuxième option est beaucoup plus simple, plus courte et plus lisible. Mais il introduit également une dépendance sur Parser, ce qui signifie qu'un bogue dans Parserpourrait échouer ce test. Donc, le test cesserait d'être un test unitaire de Expander, et je suppose que techniquement devient un test d'intégration de Parseret Expander.

Ma question est: est-il acceptable de s'appuyer principalement (ou complètement) sur ce type de tests d'intégration pour tester cette Expanderclasse?

svick
la source
3
Le fait qu'un bogue dans Parserpuisse échouer dans un autre test n'est pas un problème si vous ne vous engagez habituellement qu'à zéro échec, au contraire, cela signifie que vous avez plus de couverture Parser. Ce qui m'inquiète plutôt, c'est qu'un bogue dans Parserpourrait faire réussir ce test alors qu'il aurait dû échouer . Les tests unitaires sont là pour trouver des bugs, après tout - un test est cassé quand il ne le fait pas mais aurait dû.
Jonas Kölker

Réponses:

27

Vous allez vous retrouver à écrire beaucoup plus de tests, de comportement beaucoup plus compliqué, intéressant et utile, si vous pouvez le faire simplement. Donc, l'option qui implique

var input = new Parser().ParseStatement("x = 2 + 3 * a");

est tout à fait valable. Cela dépend d'un autre composant. Mais tout dépend de dizaines d'autres composants. Si vous vous moquez de quelque chose à un pouce de sa vie, vous dépendez probablement de nombreuses fonctionnalités de moquerie et de tests.

Les développeurs se concentrent parfois trop sur la pureté de leurs tests unitaires , ou développent des tests unitaires et des tests unitaires uniquement , sans module, intégration, stress ou autres types de tests. Tous ces formulaires sont valides et utiles, et ils relèvent tous de la responsabilité des développeurs - pas seulement du Q / A ou du personnel d'exploitation plus loin dans le pipeline.

Une approche que j'ai utilisée consiste à commencer par ces exécutions de niveau supérieur, puis à utiliser les données produites à partir de celles-ci pour construire l'expression longue du plus petit dénominateur commun du test. Par exemple, lorsque vous videz la structure de données de ce qui est inputproduit ci-dessus, vous pouvez facilement construire:

var input = new AssignStatement(
    new Variable("x"),
    new BinaryExpression(
        new Constant(2),
        BinaryOperator.Plus,
        new BinaryExpression(new Constant(3), BinaryOperator.Multiply, new Variable("a"))));

sorte de test qui teste au niveau le plus bas. De cette façon, vous obtenez un bon mélange: une poignée des tests primitifs les plus élémentaires (tests unitaires purs), mais vous n'avez pas passé une semaine à écrire des tests à ce niveau primitif. Cela vous donne le temps nécessaire pour écrire beaucoup plus, un peu moins de tests atomiques en utilisant le Parsercomme assistant. Résultat final: plus de tests, plus de couverture, plus de coins et d'autres cas intéressants, un meilleur code et une assurance qualité plus élevée.

Jonathan Eunice
la source
2
Cela est sensé - surtout en ce qui concerne le fait que tout dépend de beaucoup d'autres. Un bon test unitaire devrait tester le minimum possible. Tout ce qui est dans cette quantité minimale possible doit être testé par un test unitaire précédent. Si vous avez complètement testé Parser, vous pouvez supposer que vous pouvez utiliser en toute sécurité Parser pour tester ParseStatement
Jon Story
6
Le principal souci de pureté (je pense) est d'éviter d'écrire des dépendances circulaires dans vos tests unitaires. Si l'analyseur ou les tests de l'analyseur utilisent l'expandeur, et que ce test de l'expandeur repose sur le fonctionnement de l'analyseur, alors vous avez un risque difficile à gérer que tout ce que vous testez est que l'analyseur et l'expandeur soient cohérents , alors que ce vous vouliez faire était de tester que l'expandeur fait réellement ce qu'il est censé faire . Mais tant qu'il n'y a pas de dépendance dans l'autre sens, l'utilisation de l'analyseur dans ce test unitaire n'est pas vraiment différente de l'utilisation d'une bibliothèque standard dans un test unitaire.
Steve Jessop
@SteveJessop Bon point. Il est important d'utiliser des composants indépendants .
Jonathan Eunice
3
Quelque chose que j'ai fait dans les cas où l'analyseur lui-même est une opération coûteuse (par exemple, lire des données dans des fichiers Excel via com interop) est d'écrire des méthodes de génération de test qui exécutent l'analyseur et le code de sortie sur la console recréer la structure de données renvoyée par l'analyseur . Je copie ensuite la sortie du générateur dans des tests unitaires plus conventionnels. Cela permet de réduire la dépendance croisée dans la mesure où l'analyseur ne doit fonctionner correctement que lorsque les tests ont été créés et non à chaque exécution. (Ne pas perdre quelques secondes / test pour créer / détruire des processus Excel était un bon bonus.)
Dan Neely
+1 pour l'approche de @ DanNeely. Nous utilisons quelque chose de similaire pour stocker plusieurs versions sérialisées de notre modèle de données en tant que données de test, afin que nous puissions être sûrs que le nouveau code peut toujours fonctionner avec des données plus anciennes.
Chris Hayes
6

Bien sûr, c'est OK!

Vous avez toujours besoin d'un test fonctionnel / d'intégration qui utilise le chemin de code complet. Et le chemin de code complet dans ce cas signifie inclure l'évaluation du code généré. C'est-à-dire que vous testez que l'analyse x = 2 + 3 * aproduit du code qui, s'il est exécuté avec, a = 5sera défini xsur 17et s'il a = -2sera exécuté avec , défini xsur -4.

En dessous, vous devez faire des tests unitaires pour des bits plus petits tant que cela aide réellement à déboguer le code . Les tests les plus fins que vous aurez, la probabilité plus élevée que toute modification du code devra également changer le test, car l'interface interne change. Ces tests ont peu de valeur à long terme et ajoutent des travaux de maintenance. Il y a donc un point de rendements décroissants et vous devez vous arrêter avant.

Jan Hudec
la source
4

Les tests unitaires vous permettent de localiser des éléments spécifiques qui se cassent et où ils se sont cassés dans le code. Ils sont donc bons pour les tests à grains très fins. De bons tests unitaires aideront à réduire le temps de débogage.

Cependant, d'après mon expérience, les tests unitaires sont rarement assez bons pour vérifier le bon fonctionnement. Les tests d'intégration sont donc également utiles pour vérifier une chaîne ou une séquence d'opérations. Les tests d'intégration vous aident à passer au travers des tests fonctionnels. Comme vous l'avez cependant souligné, en raison de la complexité des tests d'intégration, il est plus difficile de trouver l'endroit spécifique dans le code où le test se casse. Il a également un peu plus de fragilité en ce sens que des échecs n'importe où dans la chaîne entraîneront l'échec du test. Cependant, vous aurez toujours cette chaîne dans le code de production, donc tester la chaîne réelle est toujours utile.

Idéalement, vous auriez les deux, mais en tout cas, avoir un test automatisé est généralement mieux que de ne pas avoir de test.

Peter Smith
la source
0

Faites beaucoup de tests sur l'analyseur et lorsque l'analyseur réussit les tests, enregistrez ces sorties dans un fichier pour simuler l'analyseur et tester l'autre composant.

Tulains Córdova
la source