Dois-je tester mes sous-classes ou ma classe parent abstraite?

12

J'ai une implémentation squelettique, comme dans l'article 18 de Effective Java (discussion approfondie ici ). C'est une classe abstraite qui fournit 2 méthodes publiques methodA () et methodB () qui appellent des méthodes de sous-classes pour "combler les lacunes" que je ne peux pas définir de manière abstraite.

Je l'ai d'abord développé en créant une classe concrète et en écrivant des tests unitaires. Lorsque la deuxième classe est arrivée, j'ai pu extraire des comportements communs, implémenter les "lacunes manquantes" dans la deuxième classe et c'était prêt (bien sûr, les tests unitaires ont été créés pour la deuxième sous-classe).

Le temps a passé et maintenant j'ai 4 sous-classes, chacune implémentant 3 méthodes protégées courtes qui sont très spécifiques à leur implémentation concrète, tandis que l'implémentation squelettique fait tout le travail générique.

Mon problème est que lorsque je crée une nouvelle implémentation, j'écris à nouveau les tests:

  • La sous-classe appelle-t-elle une méthode requise donnée?
  • Une méthode donnée a-t-elle une annotation donnée?
  • Dois-je obtenir les résultats attendus de la méthode A?
  • Dois-je obtenir les résultats attendus de la méthode B?

Bien voir les avantages de cette approche:

  • Il documente les exigences de la sous-classe grâce à des tests
  • Il peut échouer rapidement en cas de refactoring problématique
  • Testez la sous-classe dans son ensemble et via leur API publique («est-ce que methodA () fonctionne?» Et non «cette méthode protégée fonctionne-t-elle?»)

Le problème que j'ai est que les tests pour une nouvelle sous-classe sont fondamentalement une évidence: - Les tests sont tous les mêmes dans toutes les sous-classes, ils changent juste le type de retour des méthodes (l'implémentation du squelette est générique) et les propriétés I vérifier la partie affirmative du test. - Habituellement, les sous-classes n'ont pas de tests qui leur sont spécifiques

J'aime la façon dont les tests se concentrent sur le résultat de la sous-classe elle-même et le protègent contre la refactorisation, mais le fait que la mise en œuvre du test soit devenu un "travail manuel" me fait penser que je fais quelque chose de mal.

Ce problème est-il courant lors du test de la hiérarchie des classes? Comment puis-je éviter cela?

ps: J'ai pensé à tester la classe squelette, mais il semble également étrange de créer une maquette d'une classe abstraite pour pouvoir la tester. Et je pense que tout refactoring sur la classe abstraite qui change un comportement qui n'est pas attendu par la sous-classe ne serait pas remarqué aussi rapidement. Mes tripes me disent que tester la sous-classe "dans son ensemble" est l'approche préférée ici, mais n'hésitez pas à me dire que je me trompe :)

ps2: J'ai googlé et trouvé de nombreuses questions sur le sujet. L'un d'eux est celui qui a une excellente réponse de Nigel Thorne, mon cas serait son "numéro 1". Ce serait formidable, mais je ne peux pas refactoriser à ce stade, donc je devrais vivre avec ce problème. Si je pouvais refactoriser, j'aurais chaque sous-classe comme stratégie. Ce serait OK, mais je testerais toujours la "classe principale" et testerais les stratégies, mais je ne remarquerais aucune refactorisation qui rompt l'intégration entre la classe principale et les stratégies.

ps3: J'ai trouvé des réponses disant "c'est acceptable" pour tester la classe abstraite. Je suis d'accord que c'est acceptable, mais je voudrais savoir quelle est l'approche préférée dans ce cas (je commence toujours par des tests unitaires)

JSBach
la source
2
Écrivez une classe de test abstraite et faites-en hériter vos tests concrets, en reflétant la structure du code métier. Les tests sont censés tester le comportement et non l'implémentation d'une unité, mais cela ne signifie pas que vous ne pouvez pas utiliser vos connaissances internes sur le système pour atteindre cet objectif plus efficacement.
Kilian Foth du
J'ai lu quelque part que l'on ne devrait pas utiliser l'héritage sur les tests, je cherche la source pour le lire attentivement
JSBach
4
@KilianFoth êtes-vous sûr de ces conseils? Cela ressemble à une recette pour des tests unitaires fragiles, très sensibles aux changements de conception et donc peu maintenables.
Konrad Morawski

Réponses:

5

Je ne vois pas comment vous écririez des tests pour une classe de base abstraite ; vous ne pouvez pas l'instancier, vous ne pouvez donc pas en avoir une instance à tester. Vous pouvez créer une "sous-classe" concrète "factice" juste pour la tester - vous ne savez pas combien cela serait utile. YMMV.

Chaque fois que vous créez une nouvelle implémentation (classe), vous devez alors écrire des tests qui exercent cette nouvelle implémentation. Étant donné que des parties de la fonctionnalité (vos «lacunes») sont implémentées dans chaque sous-classe, cela est absolument nécessaire; la sortie de chacune des méthodes héritées peut (et devrait probablement ) être différente pour chaque nouvelle implémentation.

Phill W.
la source
Oui, ils sont différents (les types et le contenu sont différents). Techniquement parlant, je serais capable de se moquer de cette classe ou d'avoir une implémentation simple uniquement à des fins de test avec des implémentations vides. Je n'aime pas cette approche. Mais le fait que je n'ai pas "besoin" de penser à passer des tests dans de nouvelles implémentations me fait me sentir bizarre et me fait également me demander dans quelle mesure je suis confiant dans ce test (bien sûr, si je change la classe de base exprès, le les tests échouent, ce qui est une preuve que je peux leur faire confiance)
JSBach
4

En règle générale, vous créez une classe de base pour appliquer une interface. Vous voulez tester cette interface, pas les détails ci-dessous. Par conséquent, vous devez créer des tests qui instancient chaque classe individuellement, mais testez uniquement via les méthodes de la classe de base.

Les avantages de cette méthode sont que, parce que vous avez testé l'interface, vous pouvez désormais refactoriser sous l'interface. Vous pouvez trouver que le code dans une classe enfant doit être dans le parent, ou vice versa. Maintenant, vos tests prouveront que vous n'avez pas rompu le comportement lorsque vous déplacez ce code.

En général, vous devez tester le code sur la façon dont il est destiné à être utilisé. Cela signifie éviter de tester des méthodes privées et signifie souvent que votre unité testée peut être un peu plus grande que vous ne le pensiez à l'origine - peut-être un groupe de classes plutôt qu'une seule classe. Votre objectif doit être d'isoler les modifications de sorte que vous ayez rarement à modifier un test lorsque vous refactorisez une étendue donnée.

Michael K
la source