Lorsque vous effectuez des tests unitaires de la manière "appropriée", c.-à-d. Écrasez tous les appels publics et renvoyez des valeurs prédéfinies ou des simulacres, je sens que je ne teste rien en réalité. Je regarde littéralement mon code et crée des exemples basés sur le flux de logique via mes méthodes publiques.
Cela ressemble à la méthode que vous testez a besoin de plusieurs autres instances de classe (que vous devez simuler) et appelle plusieurs méthodes à elle seule.
Ce type de code est en effet difficile à tester, pour les raisons que vous avez décrites.
Ce que j’ai trouvé utile est de scinder ces cours en:
- Classes avec la "logique métier" réelle. Celles-ci utilisent peu ou pas d'appels vers d'autres classes et sont faciles à tester (value (s) in-value out).
- Classes faisant interface avec des systèmes externes (fichiers, base de données, etc.). Ceux-ci enveloppent le système externe et fournissent une interface pratique pour vos besoins.
- Des cours qui "lient tout ensemble"
Ensuite, les classes de 1. sont faciles à tester, car elles acceptent simplement des valeurs et renvoient un résultat. Dans des cas plus complexes, ces classes devront peut-être effectuer elles-mêmes des appels, mais elles n'appelleront que les classes à partir de 2. (et non directement, par exemple, une fonction de base de données). Les classes à partir de 2. sont faciles à simuler (car exposer les parties du système emballé dont vous avez besoin).
Les classes de 2. et 3. ne peuvent généralement pas être testées de manière significative (car elles ne font rien d’utile par elles-mêmes, elles ne sont que du code "collé"). OTOH, ces classes ont tendance à être relativement simples (et peu), elles devraient donc être adéquatement couvertes par des tests d'intégration.
Un exemple
Une classe
Supposons que vous ayez une classe qui récupère un prix dans une base de données, applique des remises et met à jour la base de données.
Si vous avez tout cela dans la même classe, vous devrez appeler des fonctions de base de données difficiles à imiter. En pseudocode:
1 select price from database
2 perform price calculation, possibly fetching parameters from database
3 update price in database
Les trois étapes nécessiteront un accès à la base de données, donc beaucoup de simulations (complexes), qui risquent de se rompre si le code ou la structure de la base de données change.
Séparer
Vous vous divisez en trois classes: PriceCalculation, PriceRepository, App.
PriceCalculation effectue uniquement le calcul réel et fournit les valeurs dont il a besoin. L'application relie tout:
App:
fetch price data from PriceRepository
call PriceCalculation with input values
call PriceRepository to update prices
De cette façon:
- PriceCalculation encapsule la "logique métier". C'est facile à tester car il n'appelle rien par lui-même.
- PriceRepository peut être soumis à un pseudo-test unitaire en configurant une base de données fictive et en testant les appels de lecture et de mise à jour. Il y a peu de logique, donc peu de chemins de code, vous n'avez donc pas besoin de trop de tests.
- L'application ne peut pas être testée de manière significative, car il s'agit d'un code collé. Cependant, il est également très simple, les tests d'intégration devraient donc suffire. Si, plus tard, l'application devient trop complexe, vous divisez davantage de classes de "logique métier".
Enfin, il se peut que PriceCalculation doive faire ses propres appels de base de données. Par exemple, seul PriceCalculation connaissant les données dont il a besoin, il ne peut donc pas être récupéré à l'avance par App. Ensuite, vous pouvez lui transmettre une instance de PriceRepository (ou une autre classe de référentiel), personnalisée selon les besoins de PriceCalculation. Il faudra ensuite se moquer de cette classe, mais ce sera simple, car l'interface de PriceRepository est simple, par exemple PriceRepository.getPrice(articleNo, contractType)
. Plus important encore, l'interface de PriceRepository isole PriceCalculation de la base de données. Par conséquent, les modifications apportées au schéma de la base de données ou à l'organisation des données ne risquent pas de modifier son interface et, partant, de casser les simulacres.