Je n'aime pas tester les fonctionnalités privées pour plusieurs raisons. Ils sont les suivants (ce sont les principaux points pour les personnes TLDR):
- Généralement, lorsque vous êtes tenté de tester la méthode privée d'une classe, c'est une odeur de conception.
- Vous pouvez les tester via l'interface publique (c'est ainsi que vous souhaitez les tester, car c'est ainsi que le client les appellera / les utilisera). Vous pouvez obtenir un faux sentiment de sécurité en voyant le feu vert sur tous les tests réussis pour vos méthodes privées. Il est beaucoup mieux / plus sûr de tester des cas de pointe sur vos fonctions privées via votre interface publique.
- Vous risquez une duplication sévère des tests (tests qui semblent très similaires) en testant des méthodes privées. Cela a des conséquences majeures lorsque les exigences changent, car il y aura beaucoup plus de tests que nécessaire. Cela peut également vous mettre dans une position où il est difficile de refactoriser à cause de votre suite de tests ... ce qui est l'ultime ironie, car la suite de tests est là pour vous aider à reconcevoir et à refaçonner en toute sécurité!
Je vais expliquer chacun d'eux avec un exemple concret. Il s'avère que 2) et 3) sont quelque peu intimement liés, donc leur exemple est similaire, bien que je les considère comme des raisons distinctes pour lesquelles vous ne devriez pas tester des méthodes privées.
Il est parfois nécessaire de tester des méthodes privées, il est juste important d'être conscient des inconvénients énumérés ci-dessus. Je vais l'examiner plus en détail plus tard.
Je passe également en revue pourquoi TDD n'est pas une excuse valable pour tester des méthodes privées à la toute fin.
Refactorisation de votre sortie d'un mauvais design
L'un des paternités (anti) les plus courantes que je vois est ce que Michael Feathers appelle une classe "Iceberg" (si vous ne savez pas qui est Michael Feathers, allez acheter / lire son livre "Working Effectively with Legacy Code". Il est une personne à savoir si vous êtes un ingénieur / développeur de logiciels professionnel). Il existe d'autres modèles (anti) qui provoquent ce problème, mais c'est de loin le plus courant que j'ai rencontré. Les classes "Iceberg" ont une méthode publique, et les autres sont privées (c'est pourquoi il est tentant de tester les méthodes privées). Cela s'appelle une classe "Iceberg" car il y a généralement une méthode publique solitaire, mais le reste de la fonctionnalité est caché sous l'eau sous forme de méthodes privées.
Par exemple, vous pourriez vouloir tester GetNextToken()
en l'appelant successivement sur une chaîne et en voyant qu'il renvoie le résultat attendu. Une fonction comme celle-ci mérite un test: ce comportement n'est pas anodin, surtout si vos règles de tokenisation sont complexes. Imaginons que ce ne soit pas si complexe, et nous voulons simplement enchaîner des jetons délimités par l'espace. Donc, vous écrivez un test, peut-être qu'il ressemble à quelque chose comme ça (un pseudo-code indépendant du langage, j'espère que l'idée est claire):
TEST_THAT(RuleEvaluator, canParseSpaceDelimtedTokens)
{
input_string = "1 2 test bar"
re = RuleEvaluator(input_string);
ASSERT re.GetNextToken() IS "1";
ASSERT re.GetNextToken() IS "2";
ASSERT re.GetNextToken() IS "test";
ASSERT re.GetNextToken() IS "bar";
ASSERT re.HasMoreTokens() IS FALSE;
}
Eh bien, ça a l'air plutôt sympa. Nous souhaitons nous assurer de conserver ce comportement lorsque nous apportons des modifications. Mais GetNextToken()
c'est une fonction privée ! Nous ne pouvons donc pas le tester comme ceci, car il ne compilera même pas (en supposant que nous utilisons un langage qui applique réellement public / privé, contrairement à certains langages de script comme Python). Mais qu'en est-il du changement de RuleEvaluator
classe pour suivre le principe de responsabilité unique (principe de responsabilité unique)? Par exemple, nous semblons avoir un analyseur, un tokenizer et un évaluateur coincés dans une seule classe. Ne serait-il pas préférable de simplement séparer ces responsabilités? En plus de cela, si vous créez une Tokenizer
classe, ses méthodes publiques seraientHasMoreTokens()
et GetNextTokens()
. La RuleEvaluator
classe pourrait avoir unTokenizer
objet en tant que membre. Maintenant, nous pouvons garder le même test que ci-dessus, sauf que nous testons la Tokenizer
classe au lieu de la RuleEvaluator
classe.
Voici à quoi cela pourrait ressembler en UML:
Notez que cette nouvelle conception augmente la modularité, vous pouvez donc potentiellement réutiliser ces classes dans d'autres parties de votre système (avant vous, les méthodes privées ne sont pas réutilisables par définition). C'est le principal avantage de décomposer l'évaluateur de règles, ainsi qu'une meilleure compréhensibilité / localité.
Le test serait extrêmement similaire, sauf qu'il se compilerait cette fois puisque la GetNextToken()
méthode est maintenant publique sur la Tokenizer
classe:
TEST_THAT(Tokenizer, canParseSpaceDelimtedTokens)
{
input_string = "1 2 test bar"
tokenizer = Tokenizer(input_string);
ASSERT tokenizer.GetNextToken() IS "1";
ASSERT tokenizer.GetNextToken() IS "2";
ASSERT tokenizer.GetNextToken() IS "test";
ASSERT tokenizer.GetNextToken() IS "bar";
ASSERT tokenizer.HasMoreTokens() IS FALSE;
}
Tester des composants privés via une interface publique et éviter la duplication des tests
Même si vous ne pensez pas pouvoir décomposer votre problème en moins de composants modulaires (ce que vous pouvez faire 95% du temps si vous essayez de le faire), vous pouvez simplement tester les fonctions privées via une interface publique. Plusieurs fois, les membres privés ne valent pas la peine d'être testés car ils seront testés via l'interface publique. Souvent, ce que je vois, ce sont des tests qui se ressemblent beaucoup , mais testez deux fonctions / méthodes différentes. Ce qui finit par arriver, c'est que lorsque les exigences changent (et elles le font toujours), vous avez maintenant 2 tests cassés au lieu de 1. Et si vous avez vraiment testé toutes vos méthodes privées, vous pourriez avoir plus de 10 tests cassés au lieu de 1. En bref , tester des fonctions privées (en utilisantFRIEND_TEST
ou les rendre publics ou en utilisant la réflexion) qui pourraient autrement être testés via une interface publique peuvent entraîner la duplication des tests et / ou de réflexion, vous finirez généralement par le regretter à long terme.. Vous ne voulez vraiment pas cela, car rien ne fait plus mal que votre suite de tests qui vous ralentit. Il est censé réduire le temps de développement et les coûts de maintenance! Si vous testez des méthodes privées qui sont autrement testées via une interface publique, la suite de tests peut très bien faire le contraire, et augmenter activement les coûts de maintenance et augmenter le temps de développement. Lorsque vous rendez une fonction privée publique, ou si vous utilisez quelque chose commeFRIEND_TEST
Considérez l'implémentation possible suivante de la Tokenizer
classe:
Disons que SplitUpByDelimiter()
c'est responsable du retour d'un tableau tel que chaque élément du tableau est un jeton. De plus, disons simplement que GetNextToken()
c'est simplement un itérateur sur ce vecteur. Ainsi, votre test public pourrait ressembler à ceci:
TEST_THAT(Tokenizer, canParseSpaceDelimtedTokens)
{
input_string = "1 2 test bar"
tokenizer = Tokenizer(input_string);
ASSERT tokenizer.GetNextToken() IS "1";
ASSERT tokenizer.GetNextToken() IS "2";
ASSERT tokenizer.GetNextToken() IS "test";
ASSERT tokenizer.GetNextToken() IS "bar";
ASSERT tokenizer.HasMoreTokens() IS false;
}
Imaginons que nous ayons ce que Michael Feather appelle un outil de tâtonnement . Il s'agit d'un outil qui vous permet de toucher les parties intimes des autres. Un exemple est FRIEND_TEST
de googletest, ou de réflexion si la langue le supporte.
TEST_THAT(TokenizerTest, canGenerateSpaceDelimtedTokens)
{
input_string = "1 2 test bar"
tokenizer = Tokenizer(input_string);
result_array = tokenizer.SplitUpByDelimiter(" ");
ASSERT result.size() IS 4;
ASSERT result[0] IS "1";
ASSERT result[1] IS "2";
ASSERT result[2] IS "test";
ASSERT result[3] IS "bar";
}
Eh bien, disons maintenant que les exigences changent et que la tokenisation devient beaucoup plus complexe. Vous décidez qu'un simple délimiteur de chaîne ne suffira pas et vous avez besoin d'une Delimiter
classe pour gérer le travail. Naturellement, vous vous attendez à ce qu'un test se brise, mais cette douleur augmente lorsque vous testez des fonctions privées.
Quand les tests de méthodes privées peuvent-ils être appropriés?
Il n'y a pas de "solution unique" dans le logiciel. Parfois, c'est correct (et en fait idéal) de "briser les règles". Je recommande fortement de ne pas tester les fonctionnalités privées lorsque vous le pouvez. Il y a deux situations principales quand je pense que ça va:
J'ai beaucoup travaillé avec les systèmes hérités (c'est pourquoi je suis un grand fan de Michael Feathers), et je peux dire en toute sécurité qu'il est parfois tout simplement plus sûr de simplement tester la fonctionnalité privée. Il peut être particulièrement utile pour obtenir des «tests de caractérisation» dans la ligne de base.
Vous êtes pressé et devez faire la chose la plus rapide possible ici et maintenant. À long terme, vous ne voulez pas tester des méthodes privées. Mais je dirai qu'il faut généralement un certain temps pour refactoriser les problèmes de conception. Et parfois, vous devez expédier dans une semaine. Ce n'est pas grave: faites vite et sale et testez les méthodes privées à l'aide d'un outil de tâtonnement si c'est ce que vous pensez être le moyen le plus rapide et le plus fiable pour faire le travail. Mais comprenez que ce que vous avez fait n'était pas optimal à long terme, et pensez à y revenir (ou, s'il a été oublié mais que vous le voyez plus tard, corrigez-le).
Il y a probablement d'autres situations où ça va. Si vous pensez que tout va bien et que vous avez une bonne justification, faites-le. Personne ne vous arrête. Soyez juste conscient des coûts potentiels.
L'excuse TDD
En passant, je n'aime vraiment pas que les gens utilisent TDD comme excuse pour tester des méthodes privées. Je pratique le TDD et je ne pense pas que le TDD vous oblige à le faire. Vous pouvez d'abord écrire votre test (pour votre interface publique), puis écrire du code pour satisfaire cette interface. Parfois, j'écris un test pour une interface publique, et je le satisfait en écrivant également une ou deux méthodes privées plus petites (mais je ne teste pas directement les méthodes privées, mais je sais qu'elles fonctionnent ou mon test public échouerait) ). Si je dois tester des cas limites de cette méthode privée, j'écrirai tout un tas de tests qui les atteindront via mon interface publique.Si vous ne savez pas comment frapper les cas extrêmes, c'est un signe fort que vous devez refactoriser en petits composants chacun avec leurs propres méthodes publiques. C'est un signe que vos fonctions privées en font trop et sortent du cadre de la classe .
De plus, parfois je trouve que j'écris un test qui est trop gros pour mordre pour le moment, et donc je pense "eh je reviendrai à ce test plus tard quand j'aurai plus d'une API pour travailler" (je Je vais le commenter et le garder dans le fond de mon esprit). C'est là que de nombreux développeurs que j'ai rencontrés commenceront alors à écrire des tests pour leurs fonctionnalités privées, en utilisant TDD comme bouc émissaire. Ils disent "oh, eh bien j'ai besoin d'un autre test, mais pour écrire ce test, j'ai besoin de ces méthodes privées. Par conséquent, comme je ne peux pas écrire de code de production sans écrire un test, j'ai besoin d'écrire un test pour une méthode privée. " Mais ce qu'ils doivent vraiment faire, c'est refactoriser en composants plus petits et réutilisables au lieu d'ajouter / tester un tas de méthodes privées à leur classe actuelle.
Remarque:
Il y a quelques temps, j'ai répondu à une question similaire sur le test de méthodes privées à l'aide de GoogleTest . J'ai surtout modifié cette réponse pour être plus indépendant de la langue ici.
PS Voici la conférence pertinente sur les classes d'iceberg et les outils de tâtonnement de Michael Feathers: https://www.youtube.com/watch?v=4cVZvoFGJTU