Aujourd'hui, je regardais une vidéo intitulée " JUnit Basics" et l'auteur m'a dit que, lors du test d'une méthode donnée dans votre programme, vous ne devriez pas utiliser d'autres méthodes que vous-même.
Pour être plus précis, il parlait de tester une méthode de création d’enregistrement qui prenait un nom et un nom de famille pour les arguments, et l’utilisait pour créer des enregistrements dans une table donnée. Mais il a affirmé qu'en testant cette méthode, il ne devrait pas utiliser ses autres méthodes DAO pour interroger la base de données afin de vérifier le résultat final (pour vérifier que l'enregistrement a bien été créé avec les bonnes données). Il a affirmé que pour cela, il devrait écrire du code JDBC supplémentaire pour interroger la base de données et vérifier le résultat.
Je pense comprendre l’esprit de son affirmation: vous ne voulez pas que le cas test d’une méthode dépende de l’exactitude des autres méthodes (dans ce cas, la méthode DAO), et ceci est accompli en écrivant (à nouveau) votre propre validation. / code de support (qui devrait être plus spécifique et plus ciblé, donc plus simple).
Néanmoins, des voix dans ma tête ont commencé à protester avec des arguments tels que la duplication de code, des efforts supplémentaires inutiles, etc. Je veux dire, si nous exécutons toute la batterie de tests et que nous testons minutieusement toutes nos méthodes publiques (y compris la méthode DAO), Ne serait-il pas acceptable d’utiliser certaines de ces méthodes tout en testant d’autres méthodes? Si l'un d'entre eux ne fait pas ce qu'il est censé faire, son propre cas de test échouera et nous pourrons le réparer et exécuter à nouveau la batterie de test. Pas besoin de dupliquer le code (même si le code en double est un peu plus simple) ni d'efforts inutiles.
Je suis convaincu par plusieurs applications récentes Excel - VBA que j'ai écrites (testées correctement par Rubberduck pour VBA ), dans lesquelles l'application de cette recommandation impliquerait beaucoup de travail supplémentaire, sans avantage perçu.
Pouvez-vous s'il vous plaît partager vos idées à ce sujet?
la source
Réponses:
L'esprit de sa demande est en effet correct. Le but des tests unitaires est d’isoler le code, de le tester sans aucune dépendance, afin que tout comportement erroné puisse être rapidement reconnu s’il se produit.
Cela dit, les tests unitaires sont un outil, et il est destiné à servir vos objectifs, ce n'est pas un autel à prier. Parfois, cela signifie laisser des dépendances car elles fonctionnent de manière suffisamment fiable et que vous ne voulez pas vous moquer d'eux, parfois cela signifie que certains de vos tests unitaires sont en fait assez proches, voire réellement, de tests d'intégration.
En fin de compte, vous n'obtenez pas de notes, ce qui est important, c'est le produit final du logiciel testé, mais vous devez juste être conscient du fait que vous enfreignez les règles et que vous décidez quand les compromis en valent la peine.
la source
Je pense que cela revient à la terminologie. Pour beaucoup, un "test unitaire" est une chose très spécifique et, par définition, ne peut pas avoir une condition succès / échec qui dépend de tout code en dehors de l' unité (méthode, fonction, etc.) testée. Cela inclurait une interaction avec une base de données.
Pour d'autres, le terme "test unitaire" est beaucoup plus souple et englobe tout type de test automatisé, y compris le code de test qui teste des parties intégrées de l'application.
Un puriste de test (si je peux utiliser ce terme) pourrait appeler cela un test d'intégration , à distinguer d'un test unitaire sur la base que le test dépend de plus d'une unité de code pure .
Je suppose que vous utilisez la version plus souple du terme "test unitaire" et que vous faites en réalité référence à un "test d'intégration".
En utilisant ces définitions, vous ne devriez pas dépendre de code en dehors de l'unité testée dans un "test unitaire". Dans un "test d'intégration", cependant, il est parfaitement raisonnable d'écrire un test qui teste un morceau de code spécifique, puis inspecte la base de données pour vérifier qu'elle satisfait aux critères de réussite.
Que vous deviez dépendre de tests d'intégration ou de tests unitaires, ou des deux, est un sujet de discussion beaucoup plus vaste.
la source
La réponse est oui et non...
Les tests uniques qui fonctionnent de manière isolée sont un impératif absolu, car ils vous permettent de déterminer le fonctionnement fondamental du code de bas niveau. Mais en codant de plus grandes bibliothèques, vous trouverez également des zones de code qui nécessiteront des tests couvrant plusieurs unités.
Ces tests inter-unités sont utiles pour la couverture de code et lors du test de la fonctionnalité de bout en bout, mais ils présentent quelques inconvénients dont vous devez être conscient:
À la fin de la journée, vous voulez avoir les deux… beaucoup de tests qui piègent les fonctionnalités de bas niveau et quelques-uns qui testent les fonctionnalités de bout en bout. Concentrez-vous sur la raison pour laquelle vous écrivez des tests en premier lieu. Ils sont là pour vous donner l'assurance que votre code fonctionne comme vous le souhaitez. Tout ce que vous devez faire pour y arriver est très bien.
Le fait triste mais vrai est que si vous utilisez des tests automatisés, vous avez déjà une longueur d'avance sur de nombreux développeurs. Les bonnes pratiques de test sont la première chose à faire lorsqu'une équipe de développement est confrontée à des délais serrés. Donc, tant que vous vous en tenez à vos armes et que vous écrivez des tests unitaires reflétant de manière significative les performances de votre code, je ne suis pas obsédé par la pureté de vos tests.
la source
Un problème lié à l'utilisation d'autres méthodes d'objet pour tester une condition est que vous allez manquer des erreurs qui s'annulent. Plus important encore, vous passez à côté de la douleur d'une implémentation de test difficile, et cette douleur vous apprend quelque chose sur votre code sous-jacent. Les tests douloureux signifient maintenant un entretien douloureux plus tard.
Réduisez la taille de vos unités, divisez votre classe, votre refactorisation et votre nouvelle conception jusqu'à ce qu'il soit plus facile de tester sans réimplémenter le reste de votre objet. Obtenez de l'aide si vous pensez que votre code ou vos tests ne peuvent plus être simplifiés. Essayez de penser à un collègue qui semble toujours avoir de la chance et obtenez des missions faciles à tester proprement. Demandez-lui de l'aide, car ce n'est pas de la chance.
la source
Si l'unité de fonctionnalité testée est «les données sont-elles stockées de manière persistante et récupérables», je ferais alors en sorte que le test unitaire teste - stocker dans une base de données réelle, détruire tout objet contenant des références à la base de données, puis appeler le code pour extraire le objets en arrière.
Tester si un enregistrement est créé dans la base de données semble concerner les détails de la mise en œuvre du mécanisme de stockage plutôt que de tester l'unité de fonctionnalité exposée au reste du système.
Vous voudrez peut-être raccourcir le test pour améliorer les performances en utilisant une base de données fictive, mais des sous-traitants ont fait exactement cela et sont partis à la fin de leur contrat avec un système qui a réussi ces tests, mais qui n'a en réalité rien stocké dans la base de données. entre les redémarrages du système.
Vous pouvez vous demander si «unité» dans le test d'unité désigne une «unité de code» ou une «unité de fonctionnalité», fonctionnalité créée peut-être par plusieurs unités de code de concert. Je ne trouve pas cela une distinction utile - les questions que je garderais à l'esprit sont les suivantes: "le test indique-t-il si le système fournit-il une valeur commerciale?" Et "le test est-il fragile si la mise en œuvre change?" Des tests tels que celui décrit sont utiles lorsque vous utilisez le système (vous n’avez pas encore écrit «extraire un objet de la base de données»), vous ne pouvez donc pas tester l’ensemble des fonctionnalités - mais vous ne supportez pas les modifications d’implémentation. retirez-les une fois que l'opération complète est testable.
la source
L'esprit est correct.
Idéalement, dans un test unitaire , vous testez une unité (méthode individuelle ou petite classe).
Idéalement, vous réduirez tout le système de base de données. En d'autres termes, vous exécuteriez votre méthode dans un environnement fictif et assurez-vous simplement qu'elle appelle les API de base de données correctes dans le bon ordre. Vous ne souhaitez explicitement pas tester la base de données lorsque vous testez l'une de vos propres méthodes.
Les avantages sont nombreux. Surtout, les tests deviennent vite aveuglants car vous n'avez pas besoin de vous préoccuper de la configuration d'un environnement de base de données correct et de sa restauration ultérieure.
Bien entendu, il s'agit d'un objectif ambitieux dans tout projet logiciel qui ne le fait pas dès le départ. Mais c'est l'esprit des tests unitaires .
Notez qu'il existe d'autres tests, tels que les tests de fonctionnalité, les tests de comportement, etc. qui diffèrent de cette approche - ne confondez pas "test" avec "test unitaire".
la source
Il existe au moins une raison très importante de ne pas utiliser l'accès direct à la base de données dans vos tests unitaires pour le code métier qui interagit avec la base de données: si vous modifiez l'implémentation de votre base de données, vous devrez réécrire tous ces tests unitaires. Renommez une colonne. Pour votre code, il vous suffit de modifier une seule ligne dans la définition de votre mappeur de données. Toutefois, si vous n'utilisez pas votre mappeur de données lors des tests, vous devrez également modifier chaque test unitaire faisant référence à cette colonne . Cela pourrait se transformer en une quantité de travail extraordinaire, en particulier pour les changements plus complexes qui se prêtent moins à la recherche et au remplacement.
En outre, utiliser votre mappeur de données en tant que mécanisme abstrait plutôt que de communiquer directement avec la base de données facilite la suppression complète de la dépendance à la base de données , ce qui peut ne pas être pertinent maintenant, mais lorsque vous obtenez plusieurs milliers de tests unitaires et tous. Si vous frappez une base de données, vous serez reconnaissant de pouvoir facilement les reformuler pour supprimer cette dépendance, car votre suite de tests passera de quelques minutes à exécuter à quelques secondes, ce qui peut avoir un impact considérable sur votre productivité.
Votre question indique que vous réfléchissez dans la bonne direction. Comme vous le soupçonnez, les tests unitaires sont également codés. Il est tout aussi important de les rendre faciles à gérer et à adapter aux modifications futures que pour le reste de votre base de code. Efforcez-vous de les garder lisibles et d’éliminer les doublons, et vous disposerez d’une suite de tests bien meilleure. Et une bonne suite de tests est celle qui s'habitue. Et une suite de tests utilisée permet de détecter les erreurs, tandis qu'une suite de tests non utilisée ne sert à rien.
la source
La meilleure leçon que j'ai apprise lors de l'apprentissage des tests unitaires et des tests d'intégration est de ne pas tester les méthodes, mais de tester les comportements. En d'autres termes, que fait cet objet?
Quand je le vois de cette façon, une méthode qui conserve les données et une autre méthode qui les lit commencent à être testables. Si vous testez les méthodes spécifiquement, bien sûr, vous vous retrouvez avec un test pour chaque méthode isolément, tel que:
Ce problème est dû à la perspective des méthodes de test. Ne testez pas les méthodes. Tester les comportements. Quel est le comportement de la classe WidgetDao? Il persiste les widgets. Ok, comment vérifiez-vous qu'il persiste widgets? Eh bien, quelle est la définition de la persistance? Cela signifie que lorsque vous l'écrivez, vous pouvez le relire. Ainsi, lire et écrire ensemble deviennent un test et, à mon avis, un test plus significatif.
C’est un test logique, cohérent, fiable et, à mon avis, significatif.
Les autres réponses montrent à quel point l'isolement est important, le débat pragmatique versus réaliste, et si un test unitaire peut interroger ou non une base de données. Ceux-ci ne répondent pas vraiment à la question cependant.
Pour tester si quelque chose peut être stocké, vous devez le stocker puis le relire. Vous ne pouvez pas tester si quelque chose est stocké si vous n'êtes pas autorisé à le lire. Ne testez pas le stockage des données séparément de la récupération des données. Vous allez vous retrouver avec des tests qui ne vous disent rien.
la source
Un exemple d'échec que vous préféreriez éviter, est que l'objet testé utilise une couche de mise en cache mais ne conserve pas les données comme requis. Ensuite, si vous interrogez l'objet, le message "Ouais, j'ai le nouveau nom et l'adresse", mais vous voulez que le test échoue car il n'a pas réellement fait ce qu'il était censé faire.
Sinon (et en négligeant la violation de responsabilité unique), supposons qu'il soit nécessaire de conserver une version codée en UTF-8 de la chaîne dans un champ orienté octet, mais en fait, elle persiste avec Shift JIS. Un autre composant va lire la base de données et s'attend à voir UTF-8, d'où l'exigence. Ensuite, l'aller-retour à travers cet objet indiquera le nom et l'adresse corrects car il le reconvertira à partir de Shift JIS, mais l'erreur n'est pas détectée par votre test. Espérons que cela sera détecté par un test d'intégration ultérieur, mais l'objectif des tests unitaires est de détecter les problèmes plus tôt et de savoir exactement quel composant est responsable.
Vous ne pouvez pas supposer cela, car si vous ne faites pas attention, vous écrivez un ensemble de tests mutuellement dépendants. Le "est-ce que ça sauve?" test appelle la méthode de sauvegarde testée, puis la méthode de chargement pour confirmer la sauvegarde. Le "charge-t-il?" test appelle la méthode save pour configurer le dispositif de test, puis la méthode de chargement testée pour vérifier le résultat. Les deux tests reposent sur l'exactitude de la méthode qu'ils ne testent pas, ce qui signifie qu'aucun des deux ne teste réellement l'exactitude de la méthode testée.
L'indice qu'il y a un problème ici est que deux tests supposés tester différentes unités font la même chose . Ils appellent tous deux un setter suivi d'un getter, puis vérifient que le résultat correspond à la valeur d'origine. Mais vous vouliez vérifier que le setter persiste dans les données, mais que la paire setter / getter fonctionne ensemble. Donc, vous savez que quelque chose ne va pas, il vous suffit de trouver quoi et de corriger les tests.
Si votre code est bien conçu pour les tests unitaires, vous avez au moins deux méthodes pour vérifier si les données ont bien été conservées correctement par la méthode testée:
simulez l'interface de base de données et demandez à votre simulacre d'enregistrer le fait que les fonctions appropriées ont été appelées, avec les valeurs attendues. Cela teste la méthode et fait le test unitaire classique.
transmettez-lui une base de données réelle avec exactement la même intention , pour enregistrer si les données ont été correctement conservées ou non. Mais plutôt que d'avoir une fonction fictive qui se contente de dire "ouais, j'ai les bonnes données", votre test lit directement dans la base de données et confirme que c'est correct. Ce n'est peut-être pas le test le plus pur possible, car tout un moteur de base de données est une très grosse chose à utiliser pour écrire une maquette glorifiée, avec plus de chance que j'oublie une certaine subtilité qui fait passer un test même si quelque chose ne va pas (par exemple, je ne devriez pas utiliser la même connexion de base de données pour lire que celle utilisée pour écrire, car je pourrais voir une transaction non validée). Mais il teste la bonne chose, et au moins vous savez que c'est précisément implémente toute l'interface de la base de données sans avoir à écrire de code factice!
Il s’agit donc d’un simple détail d’implémentation de test, que je lise les données de la base de données de test par JDBC ou que je moque la base de données. Quoi qu'il en soit, le fait est que je peux mieux tester l'unité en l'isolant que si je lui permettais de conspirer avec d'autres méthodes incorrectes de la même classe pour obtenir une apparence correcte, même en cas de problème. Par conséquent, je souhaite utiliser tout moyen pratique pour vérifier que les données correctes ont bien été conservées, en dehors de la confiance envers le composant dont je teste la méthode.
Si votre code n'est pas bien conçu pour les tests unitaires, vous n'avez peut-être pas le choix, car l'objet dont vous souhaitez tester la méthode risque de ne pas accepter la base de données en tant que dépendance injectée. Dans ce cas, la discussion sur le meilleur moyen d'isoler l'unité à tester passe à une discussion sur le degré de proximité possible pour isoler l'unité à tester. La conclusion est la même, cependant. Si vous pouvez éviter les complots entre unités défectueuses, alors vous le ferez, sous réserve du temps disponible et de tout ce que vous pensez qui serait plus efficace pour trouver des erreurs dans le code.
la source
C'est comme ça que j'aime le faire. Ceci est juste un choix personnel puisque cela n’a pas d’impact sur le résultat du produit, mais uniquement sur la manière de le produire.
Cela dépend des possibilités pour pouvoir ajouter des valeurs fictives dans votre base de données sans l'application que vous testez.
Supposons que vous avez deux tests: A - lecture de la base de données B - insertion dans la base de données (dépend de A)
Si A réussit, vous êtes sûr que le résultat de B dépend de la partie testée dans B et non des dépendances. Si A échoue, vous pouvez avoir un faux négatif pour B. L'erreur peut être en B ou en A. Vous ne pouvez pas savoir avec certitude jusqu'à ce que A revienne avec succès.
Je n'essaierais pas d'écrire des requêtes dans un test unitaire car vous ne devriez pas pouvoir connaître la structure de la base de données derrière l'application (sinon elle pourrait changer). Cela signifie que votre scénario de test pourrait échouer à cause de votre code lui-même. Sauf si vous écrivez un test pour le test, mais qui le testera pour le test ...
la source
En fin de compte, cela dépend de ce que vous voulez tester.
Parfois, il suffit de tester l'interface fournie par votre classe (par exemple, l'enregistrement est créé et stocké et peut être récupéré plus tard, éventuellement après un "redémarrage"). Dans ce cas, il est bon (et généralement une bonne idée) d’utiliser les méthodes fournies pour les tests. Cela permet de réutiliser les tests, par exemple si vous souhaitez modifier le mécanisme de stockage (base de données différente, base de données en mémoire pour les tests, ...).
D'autre part, dans certains cas, vous devez réellement vérifier comment les éléments sont persistants (un autre processus dépend peut-être du fait que les données sont au bon format dans cette base de données?). Maintenant, vous ne pouvez plus vous contenter de récupérer le bon enregistrement dans la classe. Vous devez plutôt vérifier qu'il est correctement stocké dans la base de données. Et le moyen le plus direct de procéder consiste à interroger la base de données elle-même.
la source