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)
la source
Réponses:
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.
la source
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.
la source