web-dev-qa-db-fra.com

Les tests unitaires ne devraient-ils pas utiliser mes propres méthodes?

Aujourd'hui, je regardais une vidéo " JUnit basics" et l'auteur a dit que lors du test d'une méthode donnée dans votre programme, vous ne devriez pas utiliser d'autres de vos propres méthodes dans le processus.

Pour être plus précis, il parlait de tester une méthode de création d'enregistrement qui prenait un nom et un nom pour les arguments, et il les utilisait pour créer des enregistrements dans une table donnée. Mais il a affirmé que dans le processus de test de cette méthode, il ne devrait pas utiliser ses autres méthodes DAO pour interroger la base de données pour vérifier le résultat final (pour vérifier cet enregistrement a en effet é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 que je comprends l'esprit de sa revendication: vous ne voulez pas que le cas de test d'une méthode dépende de l'exactitude des autres méthodes (dans ce cas, la méthode DAO), et cela se fait en écrivant (encore) votre propre validation/code de support (qui devrait être plus spécifique et ciblé, donc un code 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. ce n'est pas OK d'utiliser simplement certaines de ces méthodes tout en testant d'autres méthodes? Si l'un d'eux ne fait pas ce qu'il est censé faire, alors son propre cas de test échouera, et nous pouvons le réparer et exécuter à nouveau la batterie de test. Pas besoin de duplication de code (même si le code en double est un peu plus simple) ni d'efforts inutiles.

J'ai un fort sentiment à ce sujet en raison de plusieurs applications récentes Excel - VBA que j'ai écrites (testées correctement grâce à Rubberduck pour VBA ), où l'application de cette recommandation signifierait beaucoup de travail supplémentaire supplémentaire, sans aucun avantage perçu.

Pouvez-vous s'il vous plaît partager vos idées à ce sujet?

82
carlossierra

L'esprit de sa demande est en effet correct. Le but des tests unitaires est d'isoler le code, de le tester sans dépendances, afin que tout comportement erroné puisse être rapidement reconnu où il se produit.

Cela dit, le test unitaire est un outil, et il est destiné à servir vos objectifs, ce n'est pas un autel à prier. Parfois, cela signifie laisser des dépendances parce qu'elles fonctionnent de manière suffisamment fiable et que vous ne voulez pas vous embêter à les moquer, parfois cela signifie que certains de vos tests unitaires sont en fait assez proches sinon réellement des tests d'intégration.

En fin de compte, vous n'êtes pas noté sur ce point, ce qui est important, c'est le produit final du logiciel testé, mais vous devrez simplement faire attention lorsque vous contournez les règles et décidez quand les compromis en valent la peine.

182
whatsisname

Je pense que cela se résume à la terminologie. Pour beaucoup, un "test unitaire" est une chose très spécifique, et par définition ne peut pas avoir une condition de réussite/échec qui dépend de tout code en dehors de la nité (méthode , fonction, etc.) en cours de test. Cela comprendrait l'interaction avec une base de données.

Pour d'autres, le terme "test unitaire" est beaucoup plus lâche et englobe toute sorte 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, distingué d'un test unitaire sur la base que le test dépend de plus d'un pur - nité de code.

Je soupçonne que vous utilisez la version plus souple du terme "test unitaire" et que vous faites référence à un "test d'intégration".

En utilisant ces définitions, vous ne devez pas dépendre du code en dehors de l'unité sous test dans un "test unitaire". Dans un "test d'intégration", cependant, il est parfaitement raisonnable d'écrire un test qui exerce une partie spécifique du code, puis inspecte la base de données pour les critères de passage.

Que vous deviez dépendre des tests d'intégration ou des tests unitaires ou des deux est un sujet de discussion beaucoup plus vaste.

35
Eric King

La réponse est oui et non...

Les tests uniques qui fonctionnent isolément sont un must absolu car ils vous permettent de définir le travail de base que votre code de bas niveau fonctionne correctement. Mais en codant de plus grandes bibliothèques, vous trouverez également des zones de code qui nécessiteront des tests qui traversent les unités.

Ces tests interunités sont bons pour la couverture du code et lors du test de la fonctionnalité de bout en bout, mais ils comportent quelques inconvénients dont vous devez être conscient:

  • Sans un test isolé pour confirmer exactement ce qui casse, un test "multi-unités" ayant échoué nécessitera un dépannage supplémentaire pour déterminer exactement ce qui ne va pas avec votre code
  • S'appuyer trop sur les tests inter-unités peut vous sortir de l'état d'esprit contractuel dans lequel vous devriez toujours être en écrivant SOLID Code orienté objet. Les tests isolés ont généralement du sens parce que vos unités ne doivent être effectuer une seule action de base.
  • Les tests de bout en bout sont souhaitables mais peuvent être dangereux si un test vous oblige à écrire dans une base de données ou à effectuer une action que vous ne voudriez pas voir se produire dans un environnement de production. C'est l'une des nombreuses raisons pour lesquelles les frameworks moqueurs comme Mockito sont si populaires car ils vous permettent de simuler un objet et d'imiter un test de bout en bout sans réellement changer quelque chose que vous ne devriez pas.

À la fin de la journée, vous voulez avoir certains des deux .. beaucoup de tests qui piègent la fonctionnalité de bas niveau et quelques-uns qui testent la fonctionnalité 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 vous y attendez. 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 glisser lorsqu'une équipe de développement est confrontée à des délais stricts. Donc, tant que vous vous en tenez à vos armes et que vous écrivez des tests unitaires qui sont un reflet significatif de la façon dont votre code devrait fonctionner, je ne serais pas obsédé par la pureté de vos tests.

7
DanK

Un problème avec l'utilisation d'autres méthodes d'objet pour tester une condition est que vous manquerez des erreurs qui s'annulent mutuellement. Plus important encore, vous manquez la douleur d'une implémentation de test difficile, et cette douleur vous apprend quelque chose sur votre code sous-jacent. Des tests douloureux signifient désormais un entretien douloureux plus tard.

Rendez vos unités plus petites, divisez votre classe, refactorisez et remodelez jusqu'à ce qu'il soit plus facile de tester sans réimplémentant 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 obtenir des tâches faciles à tester proprement. Demandez-lui de l'aide, car ce n'est pas de la chance.

4
Karl Bielefeldt

Il y a au moins une raison très importante pas d'utiliser l'accès direct à la base de données dans vos tests unitaires pour le code d'entreprise qui interagit avec la base de données: si vous changez l'implémentation de votre base de données, vous devrez réécrire toutes 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. Si vous n'utilisez pas votre mappeur de données pendant le test, vous constaterez cependant que vous devez également modifier chaque test unitaire faisant référence à cette colonne. Cela pourrait se transformer en une quantité extraordinaire de travail, en particulier pour les changements plus complexes qui sont moins susceptibles d'être recherchés et remplacés.

De plus, l'utilisation de votre mappeur de données comme mécanisme abstrait plutôt que de parler directement à la base de données facilite supprimez complètement la dépendance à la base de données, ce qui n'est peut-être pas pertinent maintenant, mais lorsque vous en atteignez des milliers de tests unitaires et tous atteignent une base de données, vous serez reconnaissant de pouvoir facilement les refactoriser pour supprimer cette dépendance, car votre suite de tests passera de quelques minutes à s'exécuter en quelques secondes, ce qui peut avoir un énorme avantage sur votre productivité.

Votre question indique que vous pensez dans le bon sens. Comme vous le pensez, les tests unitaires sont également du code. Il est tout aussi important de les rendre maintenables et faciles à adapter aux changements futurs que pour le reste de votre base de code. Efforcez-vous de les garder lisibles et d'éliminer la duplication, et vous aurez une bien meilleure suite de tests pour cela. Et une bonne suite de tests est celle qui est utilisée. Et une suite de tests qui est utilisée aide à trouver des erreurs, tandis qu'une autre qui ne s'utilise pas est sans valeur.

1
Periata Breatta

La meilleure leçon que j'ai apprise en apprenant les tests unitaires et les tests d'intégration n'est pas de tester des méthodes, mais de tester des comportements. En d'autres termes, que fait cet objet?

Quand je le regarde de cette façon, une méthode qui persiste les données et une autre méthode qui les relit commencent à être testables. Si vous testez les méthodes spécifiquement, bien sûr, vous vous retrouvez avec un test pour chaque méthode de manière isolée, tel que:

@Test
public void canSaveData() {
    writeDataToDatabase();
    // what can you assert - the only expectation you can have here is that an exception was not thrown.
}

@Test
public void canReadData() {
    // how do I even get data in there to read if I cannot call the method which writes it?
}

Ce problème se produit en raison de 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 des widgets. Ok, comment vérifiez-vous que les widgets persistent? Eh bien, quelle est la définition de la persistance? Cela signifie que lorsque vous l'écrivez, vous pouvez le relire. Alors lire + écrire ensemble devient un test, et à mon avis, un test plus significatif.

@Test
public void widgetsCanBeStored() {
    Widget widget = new Widget();
    widget.setXXX.....
    // blah

    widgetDao.storeWidget(widget);
    Widget stored = widgetDao.getWidget(widget.getWidgetId());
    assertEquals(widget, stored);
}

C'est un test logique, cohérent, fiable et, à mon avis, significatif.

Les autres réponses mettent l'accent sur l'importance de l'isolement, le débat pragmatique versus réaliste, et si un test unitaire peut ou non interroger une base de données. Cela ne répond pas vraiment à la question.

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 vous retrouverez avec des tests qui ne vous disent rien.

1
Brandon

Si l'unité de fonctionnalité testée est "les données sont-elles stockées de manière persistante et récupérables", alors j'aurais le test de test unitaire qui - stocke dans une vraie base de données, détruit tous les objets contenant des références à la base de données, puis appelle le code pour récupérer le objets en arrière.

Tester si un enregistrement est créé dans la base de données semble concerner les détails d'implémentation 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 à l'aide d'une base de données fictive, mais j'ai eu des entrepreneurs qui ont fait exactement cela et ont laissé à la fin de leur contrat avec un système qui a passé ces tests, mais n'a en fait rien stocké dans la base de données entre les redémarrages du système.

Vous pouvez vous demander si "unité" dans le test unitaire désigne une "unité de code" ou une "unité de fonctionnalité", fonctionnalité peut-être créée de concert par de nombreuses unités de code. Je ne trouve pas cette distinction utile - les questions que je garderai à l'esprit sont "le test vous dit-il si le système fournit une valeur commerciale" et "le test est-il fragile si la mise en œuvre change? Des tests comme celui décrit sont utiles lorsque vous testez le système en TDD - vous n'avez pas encore écrit `` obtenir l'objet à partir de l'enregistrement de la base de données '', vous ne pouvez donc pas tester l'ensemble des fonctionnalités - mais vous êtes fragile face aux changements d'implémentation, donc je les retirer une fois que l'opération complète est testable.

1
Pete Kirkham

L'esprit est correct.

Idéalement, dans un test unitaire, vous testez un nité (une méthode individuelle ou une petite classe).

Idéalement, vous stopperiez tout le système de base de données. C'est-à-dire que vous exécutez votre méthode dans un environnement falsifié et assurez-vous simplement qu'elle appelle les bonnes API DB dans le bon ordre. Vous voulez explicitement et positivement pas vouloir tester la base de données lorsque vous testez l'une de vos propres méthodes.

Les avantages sont nombreux. Surtout, les tests deviennent aveuglants rapidement car vous n'avez pas besoin de vous soucier de configurer un environnement de base de données correct et de le restaurer par la suite.

Bien sûr, c'est un objectif noble dans tout projet logiciel qui ne le fait pas dès le départ. Mais c'est l'esprit de nit testing.

Notez qu'il existe d'autres tests, comme les tests de fonctionnalités, les tests de comportement, etc., qui sont différents de cette approche - ne confondez pas "test" avec "test unitaire".

1
AnoE

C'est comme ça que j'aime le faire. C'est juste un choix personnel car cela n'affectera pas le résultat du produit, seulement la façon de le produire.

Cela dépend des possibilités de pouvoir ajouter des valeurs fictives dans votre base de données sans l'application que vous testez.

Disons 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épendra de la partie testée en 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 être sûr jusqu'à ce que A renvoie un succès.

Je n'essaierais pas d'écrire certaines requêtes dans un test unitaire car vous ne devriez pas être en mesure de connaître la structure de la base de données derrière l'application (ou cela pourrait changer). Cela signifie que votre scénario de test a échoué en raison de votre code lui-même. Sauf si vous écrivez un test pour le test, mais qui testera le test pour le test ...

0
AxelH

Un exemple de cas d'échec que vous préférez intercepter est que l'objet testé utilise une couche de mise en cache mais ne parvient pas à conserver les données comme requis. Ensuite, si vous interrogez l'objet, il dira "yup, j'ai le nouveau nom et la nouvelle adresse", mais vous voulez que le test échoue parce qu'il n'a pas réellement fait ce qu'il était censé faire.

Alternativement (et en ignorant la violation de responsabilité unique), supposons qu'il soit nécessaire de conserver une version codée UTF-8 de la chaîne dans un champ orienté octet, mais persiste en fait 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 signalera le nom et l'adresse corrects car il les reconvertira à partir de Shift JIS, mais l'erreur n'est pas détectée par votre test. Nous espérons que cela sera détecté par un test d'intégration ultérieur, mais le but des tests unitaires est de détecter les problèmes tôt et de savoir exactement quel composant est responsable.

Si l'un d'eux ne fait pas ce qu'il est censé faire, son propre cas de test échouera et nous pouvons le réparer et exécuter à nouveau la batterie de test.

Vous ne pouvez pas supposer cela, car si vous ne faites pas attention, vous écrivez un ensemble de tests mutuellement dépendants. Le "ça sauve?" test appelle la méthode de sauvegarde qu'il teste, puis la méthode de chargement pour confirmer qu'il a été enregistré. Le "ça se charge?" test appelle la méthode save pour configurer le dispositif de test, puis la méthode load qu'il teste 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 d'eux ne teste réellement l'exactitude de la méthode qu'il teste.

L'indice qu'il y a un problème ici, c'est que deux tests qui sont censés tester des unités différentes font en fait la même chose. Ils appellent tous les deux un setter suivi d'un getter, puis vérifient que le résultat est bien la valeur d'origine. Mais vous vouliez tester que le setter persiste les données, pas 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, il existe au moins deux façons de tester si les données ont vraiment été correctement conservées par la méthode testée:

  • simulez l'interface de la base de données et demandez à votre simulateur d'enregistrer le fait que les fonctions appropriées y ont été appelées, avec les valeurs attendues. Cela teste la méthode fait ce qu'elle est censée faire, et c'est le test unitaire classique.

  • lui passer une base de données réelle avec exactement la même intention, pour enregistrer si les données ont été correctement persistées ou non. Mais plutôt que d'avoir une fonction simulée qui dit simplement "oui, j'ai les bonnes données", votre test lit directement la base de données et confirme qu'il est correct. Ce n'est peut-être pas le test le plus pur, car un moteur de base de données complet est assez important à utiliser pour écrire une maquette glorifiée, avec plus de chance que j'oublie une subtilité qui fait passer un test même si quelque chose est incorrect (par exemple, je ne devrais pas utiliser la même connexion à la base de données pour lire que pour écrire, car je pourrais voir une transaction non validée). Mais il teste la bonne chose, et au moins vous savez qu'il précisément implémente toute l'interface de la base de données sans avoir à écrire de faux code!

Il s'agit donc d'un simple détail de la mise en œuvre du test, que je lise les données de la base de données de test par JDBC ou que je me moque de 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 permets de conspirer avec d'autres méthodes incorrectes sur la même classe pour bien paraître même en cas de problème. Par conséquent, je veux utiliser tout moyen pratique pour vérifier que les données correctes ont été conservées, autre que faire confiance au composant dont je teste la méthode.

Si votre code est pas bien conçu pour les tests unitaires, vous n'avez peut-être pas le choix, car l'objet dont vous voulez tester la méthode peut ne pas accepter la base de données comme dépendance injectée. Dans ce cas, la discussion sur la meilleure façon d'isoler l'unité sous test se transforme en une discussion sur la façon dont il est possible de se rapprocher de l'isolement de l'unité sous test. La conclusion est cependant la même. Si vous pouvez éviter les complots entre les unités défectueuses, alors vous le faites, sous réserve du temps disponible et de tout ce que vous pensez qui serait plus efficace pour trouver des défauts dans le code.

0
Steve Jessop