Du point de vue TDD, suis-je une mauvaise personne si je teste contre un point de terminaison en direct au lieu d'une simulation?

16

Je suis religieusement TDD. Mes projets ont généralement une couverture de test de 85% ou mieux, avec des cas de test significatifs.

Je fais beaucoup de travail avec HBase , et l'interface client principale, HTable, est une vraie douleur à se moquer. Il me faut 3 ou 4 fois plus de temps pour écrire mes tests unitaires que pour écrire des tests qui utilisent un point de terminaison en direct.

Je sais que, philosophiquement, les tests qui utilisent des simulations devraient avoir la priorité sur les tests qui utilisent un point de terminaison en direct. Mais se moquer de HTable est une douleur sérieuse, et je ne suis pas vraiment sûr qu'il offre beaucoup d'avantages par rapport aux tests contre une instance HBase en direct.

Tous les membres de mon équipe exécutent une instance HBase à nœud unique sur leur poste de travail, et nous avons des instances HBase à nœud unique exécutées sur nos boîtes Jenkins, ce n'est donc pas un problème de disponibilité. Les tests de point de terminaison en direct prennent évidemment plus de temps à exécuter que les tests qui utilisent des simulations, mais cela ne nous intéresse pas vraiment.

En ce moment, j'écris des tests de point de terminaison en direct ET des tests sur maquette pour toutes mes classes. J'adorerais abandonner les moqueries, mais je ne veux pas que la qualité diminue en conséquence.

Qu'en pensez-vous tous?

sang-froid
la source
8
Le point de terminaison en direct n'est pas vraiment un test unitaire, n'est-ce pas? C'est un test d'intégration. Mais finalement c'est probablement une question de pragmatisme; vous pouvez soit passer du temps à écrire des simulations, soit passer du temps à écrire des fonctionnalités ou à corriger des bugs.
Robert Harvey
4
J'ai entendu des histoires de personnes supprimant des services tiers en exécutant un test unitaire contre leur propre code ... qui était connecté à un point de terminaison en direct. La limitation du débit n'est pas quelque chose que les tests unitaires font ou dont ils se soucient généralement.
14
Tu n'es pas une mauvaise personne. Vous êtes une bonne personne qui fait une mauvaise chose.
Kyralessa
15
Je suis TDD religieusement Peut-être que c'est le problème? Je ne pense pas que cette méthode est destiné à être pris que sérieux. ;)
FrustratedWithFormsDesigner
9
Suivre TDD religieusement signifierait que vous jetez le code découvert de 15%.
mouviciel

Réponses:

23
  • Ma première recommandation serait de ne pas se moquer des types que vous ne possédez pas . Vous avez mentionné que HTable était une vraie douleur à se moquer - peut-être devriez-vous l'envelopper à la place dans un adaptateur qui expose les 20% des fonctionnalités de HTable dont vous avez besoin, et se moquer de l'emballage si nécessaire.

  • Cela étant dit, supposons que nous parlons de types que vous possédez tous. Si vos tests factices se concentrent sur des scénarios de cheminement heureux où tout se passe bien, vous ne perdrez rien en les abandonnant car vos tests d'intégration testent probablement déjà les mêmes chemins exacts.

    Cependant, les tests isolés deviennent intéressants lorsque vous commencez à réfléchir à la façon dont votre système sous test devrait réagir à chaque petite chose qui pourrait se produire comme défini dans le contrat de son collaborateur, quel que soit l'objet concret réel avec lequel il parle. Cela fait partie de ce que certains appellent l'exactitude de base . Il pourrait y avoir beaucoup de ces petits cas et beaucoup plus de combinaisons d'entre eux. C'est là que les tests d'intégration commencent à devenir moche alors que les tests isolés resteront rapides et gérables.

    Pour être plus concret, que se passe-t-il si l'une des méthodes de votre adaptateur HTable renvoie une liste vide? Et si elle retourne null? Que faire s'il déclenche une exception de connexion? Il devrait être défini dans le contrat de l'adaptateur si l'une de ces choses pouvait se produire, et n'importe lequel de ses consommateurs devrait être prêt à faire face à ces situations , d'où la nécessité de tests pour eux.

Pour résumer: vous ne verrez aucune baisse de qualité en supprimant vos tests factices s'ils ont testé exactement les mêmes choses que vos tests d'intégration . Cependant, essayer d'imaginer des tests isolés supplémentaires (et des tests de contrat ) peut vous aider à réfléchir largement à vos interfaces / contrats et à améliorer la qualité en s'attaquant aux défauts qui auraient été difficiles à penser et / ou lents à tester avec des tests d'intégration.

guillaume31
la source
+1 Je trouve qu'il est beaucoup plus facile de construire des cas limites avec des tests simulés que de remplir la base de données avec ces cas.
Rob
Je suis d'accord avec la plupart de votre réponse. Cependant, je ne suis pas sûr d'être d'accord sur la partie adaptateur. HTable est une douleur à se moquer car il est assez nu. Par exemple, si vous souhaitez effectuer une opération d'obtention par lots, vous devez créer un groupe d'objets Get, les placer dans une liste, puis appeler HTable.batch (). Du point de vue moqueur, c'est une douleur sérieuse, car vous devez créer un Matcher personnalisé qui examine la liste des objets get que vous passez à HTable.batch (), puis renvoie les résultats corrects pour cette liste d'objets get (). UNE GRAVE douleur.
sangfroid
Je suppose que je pourrais créer une classe de wrapper sympa et conviviale pour HTable qui s'est occupée de tout ce ménage, mais à ce moment-là ... J'ai l'impression de construire un cadre autour de HTable, et cela devrait-il vraiment être mon travail? Habituellement "Construisons un cadre!" est un signe que je vais dans la mauvaise direction. Je pourrais passer des jours à écrire des cours pour rendre HBase plus convivial, et je ne sais pas si c'est une bonne utilisation de mon temps. De plus, alors je paasing autour d'une interface ou d'un wrapper au lieu de simplement le vieil objet HTable simple, et cela rendrait certainement mon code plus complexe.
sangfroid
Mais je suis d'accord sur votre point principal, à savoir qu'il ne faut pas écrire de simulations pour les classes qu'ils ne possèdent pas. Et certainement d'accord, il est inutile d'écrire un test simulé qui teste la même chose qu'un test d'intégration. Il semblerait que les maquettes soient les meilleures pour tester les interfaces / contrats. Merci pour les conseils - cela m'a beaucoup aidé!
sangfroid
Je ne sais pas vraiment ce que fait HTable et comment vous l'utilisez, alors ne prenez pas mon exemple de wrapper à la lettre. J'avais mentionné un emballage / adaptateur parce que je pensais que la chose à emballer était relativement petite. Vous n'avez pas besoin d'introduire une réplique un-à-un dans HTable, ce qui serait bien sûr pénible, sans parler de tout un framework - mais vous avez besoin d'une couture , d'une interface entre le domaine de votre application et celui de HTable. Il devrait reformuler certaines des fonctionnalités de HTable en termes propres à votre application. Le modèle de référentiel est une incarnation parfaite d'une telle couture en matière d'accès aux données.
guillaume31
11

philosophiquement, les tests qui utilisent des simulations devraient avoir la priorité sur les tests qui utilisent un point de terminaison en direct

Je pense à tout le moins, qui est un point de courant controverse entre les partisans TDD.

Mon point de vue personnel va au-delà de cela pour dire qu'un test basé sur une simulation est principalement un moyen de représenter une forme de contrat d'interface ; idéalement, il casse (c.-à-d. échoue) si et seulement si vous changez l'interface . Et en tant que tel, dans des langages raisonnablement fortement typés comme Java, et lorsque vous utilisez une interface définie explicitement, c'est presque entièrement superflu: le compilateur vous aura déjà dit si vous avez changé l'interface.

La principale exception est lorsque vous utilisez une interface très générique, peut-être basée sur des annotations ou des réflexions, que le compilateur n'est pas en mesure de contrôler automatiquement de manière utile. Même dans ce cas, vous devriez vérifier s'il existe un moyen de faire la validation par programme (par exemple, une bibliothèque de vérification de syntaxe SQL) plutôt qu'à la main à l'aide de simulacres.

C'est ce dernier cas que vous faites lorsque vous testez avec une base de données locale «en direct»; l'implémentation de htable entre en jeu et applique une validation beaucoup plus complète du contrat interfacve que vous ne le pensez jamais à écrire à la main.

Malheureusement, une utilisation beaucoup plus courante des tests factices est le test qui:

  • passe pour tout ce que le code était au moment où le test a été écrit
  • ne fournit aucune garantie sur les propriétés du code autres que son existence et le type d'exécution
  • échoue chaque fois que vous modifiez ce code

Ces tests doivent bien sûr être supprimés à vue.

Soru
la source
1
Je ne peux pas le sauvegarder assez. Je préfère avoir une couverture de 1 pc avec d'excellents tests plutôt qu'une couverture de 100 pc de remplissage.
Ian
3
Les tests basés sur des simulations décrivent en effet le contrat utilisé par 2 objets pour parler ensemble, mais ils vont bien au-delà de ce que le système de type d'un langage comme Java peut faire. Il ne s'agit pas seulement de signatures de méthodes, elles peuvent également spécifier des plages de valeurs valides pour les arguments ou les résultats renvoyés, quelles exceptions sont acceptables, dans quel ordre et combien de fois les méthodes peuvent être appelées, etc. Le compilateur seul ne vous avertira pas s'il existe sont des changements dans ceux-ci. En ce sens, je ne pense pas du tout qu'elles soient superflues. Voir infoq.com/presentations/integration-tests-scam pour plus d'informations sur les tests factices .
guillaume31
1
... d'accord, c'est-à-dire tester la logique autour de l'appel d'interface
Rob
1
Peut certainement ajouter des exceptions non vérifiées, des conditions préalables non déclarées et un état implicite à la liste des éléments qui rendent une interface moins typée statiquement, et justifient ainsi les tests factices au lieu d'une simple compilation. Le problème, cependant, est que lorsque ces aspects font le changement, leur spécification est implicite et réparti entre les essais de tous les clients. Qui sont susceptibles de ne pas être mis à jour, et donc de rester assis en cachant silencieusement un bug derrière une coche verte.
soru
"leur spécification est implicite": pas si vous écrivez des tests de contrat pour vos interfaces ( blog.thecodewhisperer.com/2011/07/07/contract-tests-an-example ) et que vous vous en tenez à eux lors de la configuration des simulations.
guillaume31
5

Combien de temps un test basé sur un point de terminaison prend-il pour s'exécuter qu'un test basé sur une simulation? Si c'est beaucoup plus long, alors oui, cela vaut la peine d'investir votre temps d'écriture de test pour rendre les tests unitaires plus rapides - car vous devrez les exécuter plusieurs fois. Si ce n'est pas beaucoup plus long, même si les tests basés sur les points de terminaison ne sont pas des tests unitaires «purs», tant qu'ils font un bon travail de test de l'unité, il n'y a aucune raison d'être religieux à ce sujet.

Carl Manaster
la source
4

Je suis entièrement d'accord avec la réponse de guillaume31, ne vous moquez jamais de types que vous ne possédez pas!.

Normalement, une douleur dans le test (se moquer d'une interface complexe) reflète un problème dans votre conception. Peut-être avez-vous besoin d'une abstraction entre votre modèle et votre code d'accès aux données, un exemple de formulaire utilisant une architecture hexagonale et un modèle de référentiel est le moyen le plus courant de résoudre ce type de problèmes.

Si vous voulez faire un test d'intégration pour vérifier les choses faites un test d'intégration, si vous voulez faire un test unitaire parce que vous testez votre logique, faites un test unitaire et isolez la persistance. Mais faire un test d'intégration parce que vous ne savez pas comment isoler votre logique d'un système externe (ou parce que l'isoler est une douleur) c'est une grosse odeur, vous choisissez l'intégration plutôt que l'unité pour une limitation dans votre conception et non pour un besoin réel pour tester l'intégration.

Jetez un oeil à ce formulaire de discussion Ian Cooper: http://vimeo.com/68375232 , il parle d'architecture hexagonale et de tests, il parle de quand et de quoi se moquer, un discours vraiment inspiré qui résout de nombreuses questions comme la vôtre sur le vrai TDD .

AlfredoCasado
la source
1

TL; DR - Selon moi, cela dépend de l'effort que vous finissez par dépenser pour les tests, et s'il aurait été préférable de le dépenser davantage sur votre système actuel.

Version longue:

Quelques bonnes réponses ici, mais mon point de vue est différent: les tests sont une activité économique qui doit être rentabilisée, et si le temps que vous passez n'est pas retourné dans le développement et la fiabilité du système (ou tout ce que vous cherchez à sortir) de tests) alors vous faites peut-être un mauvais investissement; vous êtes en train de construire des systèmes, pas d'écrire des tests. Par conséquent, la réduction de l'effort d'écriture et de maintenance des tests est cruciale.

Par exemple, certaines valeurs principales que je gagne des tests sont:

  • Fiabilité (et donc vitesse de développement): refactoriser le code / intégrer un nouveau framework / échanger un composant / port sur une plateforme différente, être sûr que les choses fonctionnent toujours
  • Commentaires sur la conception: rétroaction TDD / BDD classique "utilisez votre code" sur vos interfaces de bas / moyen niveau

Les tests par rapport à un point de terminaison en direct devraient toujours les fournir.

Quelques inconvénients pour les tests sur un point de terminaison en direct:

  • Configuration de l'environnement - la configuration et la normalisation de l'environnement d'exécution des tests est plus compliquée, et des configurations d'environnement subtilement différentes peuvent entraîner un comportement subtilement différent
  • Apatridie - travailler contre un point de terminaison actif peut finir par promouvoir l'écriture de tests qui reposent sur un état de point de terminaison en mutation, qui est fragile et difficile à raisonner (c.-à-d. Quand quelque chose échoue, échoue-t-il à cause d'un état étrange?)
  • L'environnement d'exécution des tests est fragile - si un test échoue, s'agit-il du test, du code ou du point de terminaison en direct?
  • Vitesse d'exécution - un point final en direct est généralement plus lent et parfois plus difficile à paralléliser
  • Créer des cas limites pour les tests - généralement trivial avec une maquette, parfois difficile avec un point de terminaison en direct (par exemple, les erreurs les plus difficiles à configurer sont les erreurs de transport / HTTP)

Si j'étais dans cette situation et que les inconvénients ne semblaient pas être un problème alors que se moquer du point de terminaison ralentissait considérablement l'écriture de mon test, je testerais un point de terminaison en direct en un clin d'œil, tant que je serais sûr de vérifiez à nouveau après un certain temps pour voir que les inconvénients ne s'avèrent pas être un problème dans la pratique.

orip
la source
1

Du point de vue des tests, certaines exigences sont absolument indispensables:

  • Les tests (unitaires ou autres) ne doivent jamais avoir un moyen de toucher les données de production
  • Les résultats d'un test ne doivent jamais affecter les résultats d'un autre test
  • Vous devez toujours partir d'une position connue

C'est un grand défi lorsque vous vous connectez à une source qui maintient un état en dehors de vos tests. Ce n'est pas du "pur" TDD, mais l'équipe de Ruby on Rails a résolu ce problème d'une manière qui pourrait être adaptée à vos besoins. Le framework de test des rails a fonctionné de cette manière:

  • La configuration des tests a été automatiquement sélectionnée lors de l'exécution des tests unitaires
  • La base de données a été créée et initialisée au début des tests unitaires en cours
  • La base de données a été supprimée après l'exécution des tests unitaires
  • Si vous utilisez SqlLite, la configuration de test a utilisé une base de données RAM

Tout ce travail a été intégré au harnais de test et il fonctionne assez bien. Il y a beaucoup plus, mais les bases sont suffisantes pour cette conversation.

Dans les différentes équipes avec lesquelles j'ai travaillé au fil du temps, nous ferions des choix qui favoriseraient le test du code même si ce n'était pas le chemin le plus correct. Idéalement, nous emballerions tous les appels vers un magasin de données avec du code que nous contrôlions. En théorie, si l'un de ces anciens projets obtenait un nouveau financement, nous pourrions revenir en arrière et le déplacer de la base de données à Hadoop en concentrant notre attention sur une poignée de classes seulement.

Les aspects importants ne sont pas de jouer avec les données de production et de vous assurer que vous testez vraiment ce que vous pensez tester. Il est vraiment important de pouvoir réinitialiser le service externe sur une base de référence connue à la demande, même à partir de votre code.

Berin Loritsch
la source