web-dev-qa-db-fra.com

TDD Red-Green-Refactor et si / comment tester des méthodes qui deviennent privées

pour autant que je le comprenne, la plupart des gens semblent d'accord pour dire que les méthodes privées ne doivent pas être testées directement, mais plutôt par tous les moyens publics qui les appellent. Je peux voir leur point, mais j'ai quelques problèmes avec cela quand j'essaie de suivre les "Trois lois du TDD", et d'utiliser le cycle "Rouge - vert - refactor". Je pense que c'est mieux expliqué par un exemple:

Pour l'instant, j'ai besoin d'un programme capable de lire un fichier (contenant des données séparées par des tabulations) et de filtrer toutes les colonnes contenant des données non numériques. Je suppose qu'il existe probablement déjà des outils simples pour le faire, mais j'ai décidé de l'implémenter à partir de zéro moi-même, principalement parce que je pensais que cela pourrait être un projet agréable et propre pour moi de m'exercer avec TDD.

Donc, d'abord, je "mets le chapeau rouge", c'est-à-dire que j'ai besoin d'un test qui échoue. J'ai pensé que j'aurais besoin d'une méthode qui trouve tous les champs non numériques dans une ligne. Donc, j'écris un test simple, bien sûr, il ne parvient pas à compiler immédiatement, alors je commence à écrire la fonction elle-même, et après quelques cycles d'avant en arrière (rouge/vert) j'ai une fonction de travail et un test complet.

Ensuite, je continue avec une fonction, "recueillirNonNumériqueColonnes" qui lit le fichier, une ligne à la fois, et appelle ma fonction "findNonNumericFields" sur chaque ligne pour rassembler toutes les colonnes qui doivent finalement être supprimées. Quelques cycles rouge-vert, et j'ai terminé, ayant à nouveau, une fonction de travail et un test complet.

Maintenant, je pense que je devrais refactoriser. Étant donné que ma méthode "findNonNumericFields" a été conçue uniquement parce que je pensais en avoir besoin lors de l'implémentation de "generateNonNumericColumns", il me semble qu'il serait raisonnable de laisser "findNonNumericFields" devenir privé. Cependant, cela casserait mes premiers tests, car ils n'auraient plus accès à la méthode qu'ils testaient.

Donc, je me retrouve avec des méthodes privées et une suite de tests qui le testent. Étant donné que tant de gens conseillent de ne pas tester les méthodes privées, j'ai l'impression de m'être peint dans un coin ici. Mais où ai-je échoué exactement?

Je suppose que j'aurais pu commencer à un niveau supérieur, en écrivant un test qui teste ce qui deviendra finalement ma méthode publique (c'est-à-dire findAndFilterOutAllNonNumericalColumns), mais cela semble quelque peu contraire à tout l'intérêt du TDD (au moins selon Uncle Bob) : Que vous devez constamment basculer entre l'écriture de tests et le code de production, et qu'à tout moment, tous vos tests ont fonctionné dans la dernière minute environ. Parce que si je commence par écrire un test pour une méthode publique, il faudra plusieurs minutes (voire des heures, voire des jours dans des cas très complexes) avant que tous les détails des méthodes privées ne fonctionnent pour que le test teste le public passe.

Alors que faire? Le TDD (avec le cycle rapide rouge-vert-refactor) n'est-il tout simplement pas compatible avec les méthodes privées? Ou y a-t-il un défaut dans ma conception?

93
Henrik Berg

Unités

Je pense que je peux identifier exactement où le problème a commencé:

J'ai pensé que j'aurais besoin d'une méthode qui trouve tous les champs non numériques dans une ligne.

Ceci devrait être immédiatement suivi en vous demandant "Est-ce que ce sera une unité testable distincte pour gatherNonNumericColumns ou une partie de la même?"

Si la réponse est "oui, séparé", alors votre plan d'action est simple: cette méthode doit être publique sur une classe appropriée, donc elle peut être testée comme une unité. Votre mentalité est quelque chose comme "J'ai besoin de tester pour chasser une méthode et j'ai aussi besoin de tester pour chasser une autre méthode"

D'après ce que vous dites cependant, vous avez pensé que la réponse est "non, une partie du même". À ce stade, votre plan ne devrait plus être d'écrire et de tester entièrement findNonNumericFields puis d'écrire gatherNonNumericColumns. Au lieu de cela, cela devrait être simplement d'écrire gatherNonNumericColumns. Pour l'instant, findNonNumericFields devrait simplement faire partie de la destination que vous avez en tête lorsque vous choisissez votre prochain test rouge et que vous effectuez votre refactoring. Cette fois, votre mentalité est "Je dois tester une méthode, et pendant ce temps, je dois garder à l'esprit que mon implémentation finale inclura probablement cette autre méthode".


Garder un cycle court

Faire ce qui précède devrait pas conduire aux problèmes que vous décrivez dans votre avant-dernier paragraphe:

Parce que si je commence par écrire un test pour une méthode publique, il faudra plusieurs minutes (voire des heures, voire des jours dans des cas très complexes) avant que tous les détails des méthodes privées ne fonctionnent pour que le test teste le public passe.

À aucun moment, cette technique ne vous oblige à écrire un test rouge qui ne deviendra vert que lorsque vous implémentez l'intégralité de findNonNumericFields à partir de zéro. Beaucoup plus probable, findNonNumericFields démarrera sous forme de code en ligne dans la méthode publique que vous testez, qui sera construit au cours de plusieurs cycles et éventuellement extrait lors d'une refactorisation.


Feuille de route

Pour donner une feuille de route approximative pour cet exemple particulier, je ne connais pas les cas de test exacts que vous avez utilisés, mais dites que vous écriviez gatherNonNumericColumns comme méthode publique. Il est alors probable que les cas de test soient les mêmes que ceux que vous avez écrits pour findNonNumericFields, chacun utilisant un tableau avec une seule ligne. Lorsque ce scénario à une ligne a été entièrement implémenté et que vous vouliez écrire un test pour vous forcer à extraire la méthode, vous écriviez un cas à deux lignes qui vous obligerait à ajouter votre itération.

44
Ben Aaronson

Beaucoup de gens pensent que les tests unitaires sont basés sur des méthodes; ce n'est pas. Il doit être basé sur la plus petite unité qui a du sens. Pour la plupart des choses, cela signifie que la classe est ce que vous devriez tester dans son ensemble. Pas de méthodes individuelles.

Maintenant, évidemment, vous appellerez des méthodes sur la classe, mais vous devriez penser que les tests s'appliquent à l'objet de boîte noire que vous avez, donc vous devriez pouvoir voir quelles que soient les opérations logiques fournies par votre classe; ce sont les choses que vous devez tester. Si votre classe est si grande que l'opération logique est trop complexe, vous avez d'abord un problème de conception qui doit être résolu en premier.

Une classe avec mille méthodes peut sembler testable, mais si vous testez chaque méthode individuellement, vous ne testez pas vraiment la classe. Certaines classes peuvent nécessiter d'être dans un certain état avant d'appeler une méthode, par exemple une classe réseau qui a besoin d'une connexion établie avant d'envoyer des données. La méthode d'envoi de données ne peut pas être considérée indépendamment de la classe entière.

Vous devez donc voir que les méthodes privées ne sont pas pertinentes pour les tests. Si vous ne pouvez pas exercer vos méthodes privées en appelant l'interface publique de votre classe, ces méthodes privées sont inutiles et ne seront pas utilisées de toute façon.

Je pense que beaucoup de gens essaient de transformer des méthodes privées en unités testables car il semble facile d'exécuter des tests pour elles, mais cela pousse la granularité du test trop loin. Martin Fowler dit

Bien que je commence par l'idée que l'unité est une classe, je prends souvent un tas de classes étroitement liées et les traite comme une seule unité

ce qui a beaucoup de sens pour un système orienté objet, les objets étant conçus pour être des unités. Si vous voulez tester des méthodes individuelles, vous devriez peut-être plutôt créer un système procédural comme C, ou une classe entièrement composée de fonctions statiques.

66
gbjbaanb

Le fait que vos méthodes de collecte de données soient suffisamment complexes pour mériter des tests et suffisamment séparés de votre objectif principal pour être des méthodes qui leur sont propres plutôt que faire partie de certains boucle pointe vers la solution: rendez ces méthodes pas privées, mais les membres de certaines autres classes qui fournissent la collecte/filtrage/tabulation Fonctionnalité.

Ensuite, vous écrivez des tests pour les aspects stupides de transfert de données de la classe d'assistance (par exemple, "distinguer les chiffres des caractères") à un endroit, et les tests pour votre objectif principal (par exemple, "obtenir les chiffres des ventes") à un autre endroit, et vous ne pas besoin de répéter les tests de filtrage de base dans les tests pour votre logique métier normale.

De manière générale, si votre classe qui fait une chose contient du code complet pour faire une autre chose qui est nécessaire, mais distincte de son objectif principal, ce code devrait vivre dans une autre classe et être appelé via des méthodes publiques. Il ne devrait pas être caché dans les coins privés d'une classe qui ne contient que accidentellement ce code. Cela améliore la testabilité et la compréhensibilité en même temps.

51
Kilian Foth

Personnellement, je pense que vous êtes allé trop loin dans la mentalité d'implémentation lorsque vous avez écrit les tests. Vous supposé vous auriez besoin de certaines méthodes. Mais en avez-vous vraiment besoin pour faire ce que la classe est censée faire? La classe échouerait-elle si quelqu'un venait et les refaçonnait en interne? Si vous étiez en utilisant la classe (et cela devrait être l'état d'esprit du testeur à mon avis), vous vous soucieriez vraiment moins s'il existe une méthode explicite pour vérifier les nombres.

Vous devez tester l'interface publique d'une classe. L'implémentation privée est privée pour une raison. Il ne fait pas partie de l'interface publique car il n'est pas nécessaire et peut changer. C'est un détail d'implémentation.

Si vous écrivez des tests sur l'interface publique, vous n'obtiendrez jamais réellement le problème que vous avez rencontré. Soit vous pouvez créer des cas de test pour l'interface publique qui couvrent vos méthodes privées (super), soit vous ne pouvez pas. Dans ce cas, il serait peut-être temps de réfléchir sérieusement aux méthodes privées et peut-être de les supprimer ensemble si elles ne sont pas accessibles de toute façon.

29
nvoigt

Vous ne faites pas TDD en fonction de ce que vous attendez de la classe en interne.

Vos cas de test doivent être basés sur ce que la classe/fonctionnalité/programme doit faire au monde extérieur. Dans votre exemple, l'utilisateur appellera-t-il jamais votre classe de lecteur avec find all the non-numerical fields in a line?

Si la réponse est "non", c'est un mauvais test à écrire en premier lieu. Vous voulez écrire le test sur la fonctionnalité à une classe/interface niveau - pas le niveau "ce que la méthode de classe devra implémenter pour que cela fonctionne", ce qui est votre test.

Le flux de TDD est:

  • rouge (que fait la classe/objet/fonction/etc au monde extérieur)
  • vert (écrire le code minimal pour faire fonctionner cette fonction de monde externe)
  • refactor (quel est le meilleur code pour que cela fonctionne)

Ce n'est PAS à faire "parce que j'aurai besoin de X à l'avenir comme méthode privée, laissez-moi l'implémenter et le tester d'abord." Si vous vous trouvez en train de faire cela, vous faites incorrectement l'étape "rouge". Cela semble être votre problème ici.

Si vous vous retrouvez à écrire fréquemment des tests pour des méthodes qui deviennent des méthodes privées, vous faites l'une des choses suivantes:

  • Vous ne comprenez pas assez bien votre interface/cas d'utilisation au niveau public pour écrire un test pour eux
  • Changer considérablement votre conception et refactoriser plusieurs tests (ce qui peut être une bonne chose, selon que cette fonctionnalité est testée dans des tests plus récents)
11
enderland

Vous rencontrez une idée fausse commune avec les tests en général.

La plupart des gens qui sont nouveaux dans les tests commencent à penser de cette façon:

  • écrire un test pour la fonction F
  • mettre en œuvre F
  • écrire un test pour la fonction G
  • implémenter G à l'aide d'un appel à F
  • écrire un test pour une fonction H
  • implémenter H à l'aide d'un appel à G

etc.

Le problème ici est que vous n'avez en fait aucun test unitaire pour la fonction H. Le test qui est censé tester H teste en fait H, G et F en même temps.

Pour résoudre ce problème, vous devez réaliser que les unités testables ne doivent jamais dépendre les unes des autres, mais plutôt de leurs interfaces. Dans votre cas, lorsque les unités sont de simples fonctions, les interfaces ne sont que leur signature d'appel. Vous devez donc implémenter G de telle manière qu'il puisse être utilisé avec la fonction any ayant la même signature que F.

La façon exacte dont cela peut être fait dépend de votre langage de programmation. Dans de nombreux langages, vous pouvez passer des fonctions (ou des pointeurs vers celles-ci) comme arguments à d'autres fonctions. Cela vous permettra de tester chaque fonction isolément.

9
initcrash

Les tests que vous écrivez pendant le développement piloté par les tests sont censés garantir qu'une classe implémente correctement son API publique, tout en s'assurant simultanément que cette API publique est facile à tester et à utiliser.

Vous pouvez certainement utiliser des méthodes privées pour implémenter cette API, mais il n'est pas nécessaire de créer des tests via TDD - la fonctionnalité sera testée car l'API publique fonctionnera correctement.

Supposons maintenant que vos méthodes privées soient suffisamment compliquées pour qu'elles méritent des tests autonomes - mais elles n'ont aucun sens dans le cadre de l'API publique de votre classe d'origine. Eh bien, cela signifie probablement qu'elles devraient en fait être des méthodes publiques sur une autre classe - une classe dont votre classe d'origine tire parti dans sa propre implémentation.

En testant uniquement l'API publique, vous simplifiez à l'avenir la modification des détails d'implémentation. Les tests inutiles ne vous ennuieront que plus tard lorsqu'ils devront être réécrits pour prendre en charge une refactorisation élégante que vous venez de découvrir.

8
Bill Michell

Je pense que la bonne réponse est la conclusion à laquelle vous êtes parvenu en commençant par les méthodes publiques. Vous commenceriez par écrire un test qui appelle cette méthode. Cela échouerait donc vous créez une méthode avec ce nom qui ne fait rien. Ensuite, vous avez peut-être raison d'un test qui vérifie une valeur de retour.

(Je ne sais pas exactement ce que fait votre fonction. Renvoie-t-elle une chaîne avec le contenu du fichier avec les valeurs non numériques supprimées?)

Si votre méthode renvoie une chaîne, vous vérifiez cette valeur de retour. Vous continuez donc simplement à le construire.

Je pense que tout ce qui se passe dans une méthode privée devrait être dans la méthode publique à un moment donné de votre processus, puis uniquement déplacé dans la méthode privée dans le cadre d'une étape de refactoring. Pour autant que je sache, la refactorisation ne nécessite pas d'échecs de tests. Vous avez seulement besoin d'échecs de tests lors de l'ajout de fonctionnalités. Il vous suffit d'exécuter vos tests après la refactorisation pour vous assurer qu'ils réussissent tous.

4
Matt Dyer

c'est comme si je m'étais peinte dans un coin ici. Mais où ai-je échoué exactement?

Il y a un vieil adage.

Lorsque vous échouez à planifier, vous prévoyez d'échouer.

Les gens semblent penser que lorsque vous TDD, vous vous asseyez, écrivez des tests, et la conception se fera comme par magie. Ce n'est pas vrai. Vous devez avoir un plan de haut niveau. J'ai constaté que j'obtiens mes meilleurs résultats de TDD lorsque je conçois l'interface (API publique) en premier. Personnellement, je crée un interface réel qui définit d'abord la classe.

halètement J'ai écrit du "code" avant d'écrire des tests! Et bien non. Non. J'ai écrit un contrat à suivre, un design . Je soupçonne que vous pourriez obtenir des résultats similaires en notant un diagramme UML sur du papier millimétré. Le fait est que vous devez avoir un plan. TDD n'est pas une licence pour pirater n'importe quoi de bon gré sur un morceau de code.

J'ai vraiment l'impression que "Test First" est un terme inapproprié. Design First then test.

Bien sûr, veuillez suivre les conseils que d'autres ont donnés sur l'extraction de plus de classes de votre code. Si vous ressentez fortement le besoin de tester les composants internes d'une classe, extrayez ces composants internes dans une unité facilement testée et injectez-la.

3
RubberDuck

N'oubliez pas que les tests peuvent également être refactorisés! Si vous rendez une méthode privée, vous réduisez l'API publique, et il est donc parfaitement acceptable de jeter certains tests correspondants pour cette "fonctionnalité perdue" (AKA complexité réduite).

D'autres ont déclaré que votre méthode privée serait appelée dans le cadre de vos autres tests d'API, ou qu'elle serait inaccessible et donc supprimable. En fait, les choses sont plus fines si l'on pense à chemins d'exécution.

Par exemple, si nous avons une méthode publique qui effectue la division, nous pourrions vouloir tester le chemin qui aboutit à la division par zéro. Si nous rendons la méthode privée, nous avons le choix: soit nous pouvons considérer le chemin par division par zéro, o nous pouvons éliminer ce chemin en considérant comment il est appelé par les autres méthodes.

De cette façon, nous pouvons jeter certains tests (par exemple, diviser par zéro) et refactoriser les autres en termes d'API publique restante. Bien sûr, dans un monde idéal, les tests existants prennent en charge tous les chemins restants, mais la réalité est toujours un compromis;)

2
Warbo

Il y a des moments où une méthode privée peut devenir une méthode publique d'une autre classe.

Par exemple, vous pouvez avoir des méthodes privées qui ne sont pas thread-safe et laisser la classe dans un état temporaire. Ces méthodes peuvent être déplacées dans une classe distincte qui est détenue en privé par votre première classe. Donc, si votre classe est une file d'attente, vous pouvez avoir une classe InternalQueue qui a des méthodes publiques et la classe Queue détient l'instance InternalQueue en privé. Cela vous permet de tester la file d'attente interne et de clarifier également les opérations individuelles sur la file d'attente interne.

(Ceci est plus évident lorsque vous imaginez qu'il n'y a pas de classe List et que vous essayez d'implémenter les fonctions List en tant que méthodes privées dans la classe qui les utilise.)

2
Thomas Andrews

J'ai vécu cela et j'ai ressenti ta douleur.

Ma solution était de:

arrêtez de traiter des tests comme la construction d'un monolithe.

N'oubliez pas que lorsque vous avez écrit un ensemble de tests, disons 5, pour éliminer certaines fonctionnalités, vous n'avez pas besoin de conserver tous ces tests, surtout lorsque cela fait partie d'autre chose.

Par exemple, j'ai souvent:

  • test de bas niveau 1
  • code pour y répondre
  • test de bas niveau 2
  • code pour y répondre
  • test de bas niveau 3
  • code pour y répondre
  • test de bas niveau 4
  • code pour y répondre
  • test de bas niveau 5
  • code pour y répondre

alors j'ai

  • test de bas niveau 1
  • test de bas niveau 2
  • test de bas niveau 3
  • test de bas niveau 4
  • test de bas niveau 5

Cependant, si j'ajoute maintenant des fonctions de niveau supérieur qui l'appellent, qui ont beaucoup de tests, je pourrait pouvoir maintenant réduire ces tests de bas niveau pour être simplement:

  • test de bas niveau 1
  • test de bas niveau 5

Le diable est dans les détails et la capacité de le faire dépendra des circonstances.

0
Michael Durrant

Je me demande pourquoi votre langue n'a que deux niveaux de confidentialité, entièrement public et totalement privé.

Pouvez-vous faire en sorte que vos méthodes non publiques soient accessibles aux packages ou quelque chose comme ça? Ensuite, mettez vos tests dans le même package et profitez de tester le fonctionnement interne qui ne fait pas partie de l'interface publique. Votre système de build exclura les tests lors de la construction d'un binaire de version.

Bien sûr, parfois, vous devez disposer de méthodes vraiment privées, qui ne sont accessibles qu'à la classe de définition. J'espère que toutes ces méthodes sont très petites. En général, garder les méthodes petites (par exemple en dessous de 20 lignes) aide beaucoup: tester, maintenir et simplement comprendre le code devient plus facile.

0
9000

J'ai occasionnellement supplanté des méthodes privées pour les protéger afin de permettre des tests plus fins (plus serrés que l'API publique exposée). Cela devrait être l'exception (espérons-le très rare) plutôt que la règle, mais cela peut être utile dans certains cas spécifiques que vous pouvez rencontrer. En outre, c'est quelque chose que vous ne voudriez pas du tout considérer lors de la construction d'une API publique, davantage de "triche" que l'on peut utiliser sur un logiciel à usage interne dans ces rares situations.

0
Brian Knoblauch