Y a-t-il lieu de faire des tests unitaires qui protègent et ridiculisent tout le public?

59

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. Et chaque fois que la mise en œuvre change, je dois changer ces tests, encore une fois, sans vraiment avoir le sentiment d’accomplir quelque chose d’utile (que ce soit à moyen ou à long terme). Je fais aussi des tests d'intégration (y compris les chemins non heureux) et les temps de test plus longs ne me dérangent pas. Avec ceux-ci, j'ai l'impression de tester des régressions, car ils en ont capturé plusieurs, alors que tous les tests unitaires me montrent que la mise en œuvre de ma méthode publique a changé, ce que je connais déjà.

Les tests unitaires sont un sujet vaste et j'ai l'impression que c'est moi qui ne comprends pas quelque chose ici. Quel est l'avantage décisif des tests unitaires par rapport aux tests d'intégration (hors temps)?

passionné
la source
4
Mes deux cents: Ne vous moquez
Juan Mendes
1
Voir aussi "Le moqueur est une odeur de code"
user949300

Réponses:

37

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:

  1. 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).
  2. 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.
  3. 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.

sleske
la source
5
Je pensais être seul à ne pas comprendre l'intérêt de tout tester, merci
passionné
4
Je ne suis pas d'accord quand vous dites que les classes de type 3 sont peu nombreuses, j'ai l'impression que la plupart de mes codes sont de type 3 et qu'il n'y a presque pas de logique métier. C'est ce que je veux dire: stackoverflow.com/questions/38496185/…
Rodrigo Ruiz
27

Quel est l'avantage décisif des tests unitaires par rapport aux tests d'intégration?

C'est une fausse dichotomie.

Les tests unitaires et les tests d'intégration ont deux objectifs similaires, mais différents. Le but des tests unitaires est de vous assurer que vos méthodes fonctionnent. Concrètement, les tests unitaires permettent de s'assurer que le code respecte le contrat défini par les tests unitaires. Cela est évident dans la conception des tests unitaires: ils indiquent spécifiquement ce que le code est censé faire et affirment que le code le fait.

Les tests d'intégration sont différents. Les tests d'intégration exercent l'interaction entre les composants logiciels. Vous pouvez avoir des composants logiciels qui passent tous leurs tests et qui échouent tout de même les tests d'intégration car ils n'interagissent pas correctement.

Toutefois, si les tests unitaires présentent un avantage décisif, c’est que les tests unitaires sont beaucoup plus faciles à configurer et demandent beaucoup moins de temps et d’efforts que les tests d’intégration. Lorsqu'ils sont utilisés correctement, les tests unitaires encouragent le développement de code "testable", ce qui signifie que le résultat final sera plus fiable, plus facile à comprendre et à maintenir. Le code testable a certaines caractéristiques, comme une API cohérente, un comportement répétable et il renvoie des résultats faciles à affirmer.

Les tests d'intégration sont plus difficiles et plus coûteux, car vous avez souvent besoin de moqueries élaborées, d'une configuration complexe et d'assertions difficiles. Au plus haut niveau d’intégration système, imaginez-vous essayer de simuler une interaction humaine dans une interface utilisateur. Des systèmes logiciels entiers sont consacrés à ce type d’automatisation. Et c’est l’automatisation que nous recherchons; les tests humains ne sont pas reproductibles et ne se développent pas comme les tests automatisés.

Enfin, les tests d'intégration ne garantissent pas la couverture du code. Combien de combinaisons de boucles de code, de conditions et de branches testez-vous avec vos tests d'intégration? Tu sais vraiment? Il existe des outils que vous pouvez utiliser avec les tests unitaires et les méthodes testées pour vous indiquer le niveau de couverture de code dont vous disposez et la complexité cyclomatique de votre code. Mais ils ne fonctionnent vraiment bien qu'au niveau de la méthode, où vivent les tests unitaires.


Si vos tests changent chaque fois que vous effectuez une refactorisation, le problème est différent. Les tests unitaires sont censés consister à documenter ce que fait votre logiciel, à prouver qu'il le fait, puis à prouver qu'il le fait à nouveau lorsque vous restructurez l'implémentation sous-jacente. Si votre API change ou si vous avez besoin que vos méthodes changent en fonction d'une modification de la conception du système, c'est ce qui est censé se produire. Si cela se produit souvent, pensez à écrire vos tests avant d'écrire du code. Cela vous obligera à réfléchir à l'architecture globale et vous permettra d'écrire du code avec l'API déjà établie.

Si vous passez beaucoup de temps à écrire des tests unitaires pour du code trivial comme

public string SomeProperty { get; set; }

alors vous devriez réexaminer votre approche. Le test unitaire est censé tester le comportement, et il n'y a pas de comportement dans la ligne de code ci-dessus. Vous avez cependant créé une dépendance dans votre code quelque part, car cette propriété sera certainement référée ailleurs dans votre code. Au lieu de cela, pensez à écrire des méthodes qui acceptent la propriété nécessaire en tant que paramètre:

public string SomeMethod(string someProperty);

Désormais, votre méthode ne dépend d'aucun élément en dehors d'elle-même et est maintenant plus testable, car elle est complètement autonome. Certes, vous ne pourrez pas toujours faire cela, mais cela déplace votre code dans le sens où il est plus testable, et cette fois, vous écrivez un test unitaire du comportement réel.

Robert Harvey
la source
2
Je suis conscient du fait que les tests unitaires et d'intégration testent différents objectifs sur le serveur. Cependant, je ne comprends toujours pas en quoi les tests unitaires sont utiles si vous stubez et simulez tous les appels publics effectués par ces tests unitaires. Je comprendrais que «le code remplisse le contrat défini par les tests unitaires», s’il n’y avait pas de moignons et de faux; mes tests unitaires sont littéralement des réflexions de logique à l'intérieur de méthodes que je teste. Vous (I) ne testez pas vraiment quoi que ce soit, vous ne faites que regarder votre code et le «convertir» en tests. En ce qui concerne la difficulté à automatiser et à couvrir le code, je suis actuellement en train de faire Rails, et les deux sont bien pris en charge.
Enthousiasme
2
Si vos tests ne sont que des réflexions de la logique de la méthode, vous vous trompez. Vos tests unitaires doivent essentiellement transmettre une valeur à la méthode, accepter une valeur de retour et faire une assertion sur la valeur de cette valeur de retour. Aucune logique n'est requise pour le faire.
Robert Harvey
2
Cela a du sens, mais il faut tout de même interrompre tous les autres appels publics (db, certains "globals", comme le statut de l'utilisateur actuel, etc.) et aboutir à un code de test en suivant la logique de la méthode.
Enthousiasme
1
Je suppose donc que les tests unitaires concernent essentiellement des éléments isolés qui confirment en quelque sorte "un ensemble d'entrées -> un ensemble de résultats attendus"?
Enthousiasme
1
Mon expérience dans la création de nombreux tests unitaires et d’intégration (sans parler des outils avancés de moquage, d’intégration et de couverture de code utilisés par ces tests) contredit la plupart de vos affirmations: 1) "Le but des tests unitaires est de vous assurer que votre le code fait ce qu’il est supposé ": il en va de même pour les tests d’intégration (encore plus); 2) "les tests unitaires sont beaucoup plus faciles à configurer": non, ils ne le sont pas (bien souvent, ce sont les tests d'intégration qui sont plus faciles); 3) "Lorsqu'ils sont utilisés correctement, les tests unitaires encouragent le développement de" codes "testables": idem pour les tests d'intégration; (continue)
Rogério le
4

Les tests unitaires avec mock doivent s’assurer que l’implémentation de la classe est correcte. Vous simulez les interfaces publiques des dépendances du code que vous testez. De cette façon, vous avez le contrôle sur tout ce qui est externe à la classe et vous êtes certain qu'un test qui échoue est dû à quelque chose de interne à la classe et non à l'un des autres objets.

Vous testez également le comportement de la classe sous test et non la mise en œuvre. Si vous refactorisez le code (en créant de nouvelles méthodes internes, etc.), les tests unitaires ne doivent pas échouer. Mais si vous modifiez ce que fait la méthode publique, les tests doivent absolument échouer car vous avez modifié le comportement.

On dirait aussi que vous écrivez les tests après avoir écrit le code, essayez plutôt d'écrire les premiers tests. Essayez de décrire le comportement que la classe devrait avoir, puis écrivez la quantité minimale de code nécessaire à la réussite des tests.

Les tests unitaires et les tests d'intégration sont utiles pour garantir la qualité de votre code. Les tests unitaires examinent chaque composant séparément. Et les tests d'intégration garantissent que tous les composants interagissent correctement. Je veux avoir les deux types dans ma suite de tests.

Les tests unitaires m'ont aidé dans mon développement car je peux me concentrer sur une partie de l'application à la fois. Se moquant des composants que je n'ai pas encore fabriqués. Ils sont également intéressants pour la régression, car ils documentent tous les bugs de la logique que j'ai trouvée (même dans les tests unitaires).

MISE À JOUR

La création d'un test qui s'assure uniquement que les méthodes sont appelées a une valeur en ce sens que vous vous assurez que les méthodes sont effectivement appelées. Si vous écrivez d'abord vos tests, vous disposez notamment d'une liste de contrôle des méthodes à utiliser. Comme ce code est assez procédural, vous n’avez pas grand chose à vérifier si ce n’est que les méthodes sont appelées. Vous protégez le code pour le changement dans le futur. Lorsque vous devez appeler une méthode avant l'autre. Ou qu'une méthode soit toujours appelée même si la méthode initiale lève une exception.

Le test de cette méthode peut ne jamais changer ou ne peut changer que lorsque vous modifiez les méthodes. Pourquoi est-ce une mauvaise chose? Cela aide à renforcer l'utilisation des tests. Si vous devez corriger un test après avoir changé le code, vous aurez l'habitude de changer les tests avec le code.

Schleis
la source
Et si une méthode n'appelle rien de privé, cela ne sert à rien que l'unité la teste, n'est-ce pas?
Enthousiasmes
Si la méthode est privée, vous ne la testez pas explicitement, elle devrait être testée via l'interface publique. Toutes les méthodes publiques doivent être testées pour s'assurer que le comportement est correct.
Schleis
Non, je veux dire si une méthode publique n'appelle rien de privé, est-il judicieux de tester cette méthode publique?
Enthousiasme
Oui. La méthode fait quelque chose, n'est-ce pas? Donc, il devrait être testé. Du point de vue des tests, je ne sais pas s'il utilise quelque chose de privé. Je sais juste que si je fournis l'entrée A, je devrais obtenir la sortie B.
Schleis
Oh oui, la méthode fait quelque chose et que quelque chose appelle des méthodes publiques (et seulement cela). Ainsi, la manière dont vous testez correctement consiste à stub les appels avec des valeurs de retour, puis à définir les attentes en matière de message. Que testez-vous EXACTEMENT dans ce cas? Que les bons appels sont faits? Eh bien, vous avez écrit cette méthode et vous pouvez la regarder et voir exactement ce qu'elle fait. Je pense que les tests unitaires sont plus appropriés pour les méthodes isolées qui sont supposées être utilisées, telles que 'entrée -> sortie'. Vous pouvez donc créer de nombreux exemples, puis effectuer des tests de régression lorsque vous effectuez une refactorisation.
Enthousiasme
3

Je rencontrais une question similaire - jusqu'à ce que je découvre la puissance des tests de composants. En bref, ils sont identiques aux tests unitaires, sauf que vous ne vous moquez pas par défaut, mais utilisez des objets réels (idéalement via une injection de dépendance).

De cette façon, vous pouvez créer rapidement des tests robustes avec une bonne couverture de code. Pas besoin de mettre à jour vos mocks tout le temps. Il est peut-être un peu moins précis que les tests unitaires avec 100% de simulacres, mais le temps et l’argent économisés compensent ce manque. La seule chose dont vous avez vraiment besoin pour utiliser des simulacres ou des agencements est un système de stockage ou des services externes.

En fait, les moqueries excessives sont un anti-modèle: les anti-modèles et les moqueurs de TDD sont diaboliques .

lastzero
la source
0

Bien que l'opération ait déjà marqué une réponse, j'ajoute simplement mes 2 centimes ici.

Quel est l'avantage décisif des tests unitaires par rapport aux tests d'intégration (hors temps)?

Et aussi en réponse à

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é.

Il y a un utile mais pas exactement ce que l'OP a demandé:

Les tests unitaires fonctionnent, mais il y a encore des bugs?

D'après ma petite expérience des suites de tests, je comprends que les tests unitaires consistent toujours à tester les fonctionnalités les plus élémentaires d'une classe en termes de méthodes. À mon avis, chaque méthode, qu'elle soit publique, privée ou interne, mérite d'avoir des tests unitaires dédiés. Même dans mon expérience récente, j'avais une méthode publique qui appelait une autre petite méthode privée. Donc, il y avait deux approches:

  1. ne créez pas de test unitaire pour une méthode privée.
  2. créer un ou plusieurs tests unitaires pour une méthode privée.

Si vous pensez logiquement, le point d’avoir une méthode privée est: la méthode publique principale devient trop volumineuse ou malpropre. Afin de résoudre ce problème, vous reformulez judicieusement et créez de petits morceaux de code qui méritent d'être des méthodes privées distinctes, ce qui rend ensuite votre méthode publique principale moins volumineuse. Vous refactorisez en gardant à l'esprit que cette méthode privée pourrait être réutilisée ultérieurement. Il peut y avoir des cas dans lesquels il n'y a pas d'autre méthode publique dépendant de cette méthode privée, mais qui sait à propos de l'avenir.


Considérant le cas où la méthode privée est réutilisée par beaucoup d'autres méthodes publiques.

Donc, si j'avais choisi l'approche 1: j'aurais dupliqué les tests unitaires et ils auraient été compliqués, car vous disposiez d'un nombre de tests unitaires pour chaque branche de la méthode publique ainsi que de la méthode privée.

Si j'avais choisi l'approche 2: le code écrit pour les tests unitaires serait relativement moins, et il serait beaucoup plus facile à tester.


Prise en compte du cas où la méthode privée n'est pas réutilisée Il est inutile d'écrire un test unitaire séparé pour cette méthode.

En ce qui concerne les tests d'intégration , ils ont tendance à être exhaustifs et de plus haut niveau. Ils vous diront que si vous donnez votre avis, toutes vos classes doivent en arriver à cette conclusion finale. Pour en savoir plus sur l'utilité des tests d'intégration, veuillez consulter le lien mentionné.

Shankbond
la source