Les tests d'intégration sont-ils censés répéter tous les tests unitaires?


37

Disons que j'ai une fonction (écrite en Ruby, mais que tout le monde devrait la comprendre):

def am_I_old_enough?(name = 'filip')
   person = Person::API.new(name)
   if person.male?
      return person.age > 21
   else
      return person.age > 18
   end
end

Lors des tests unitaires, je créerais quatre tests couvrant tous les scénarios. Chacun utilisera un Person::APIobjet fictif avec les méthodes stubbed male?et age.

Il s’agit maintenant d’écrire des tests d’intégration. Je suppose que Person :: API ne devrait plus être ridiculisé. Je créerais donc exactement les mêmes quatre cas de test, mais sans se moquer de l'objet Person :: API. Est-ce exact?

Si oui, alors à quoi sert-il d'écrire des tests unitaires, si je pouvais juste écrire des tests d'intégration qui me donnent plus de confiance (alors que je travaille sur des objets réels, pas des moignons ou des imitations)?


3
Eh bien, l’un des points est qu’en moquant / en testant l’unité, vous pouvez isoler tous les problèmes de votre code. Si un test d'intégration échoue, vous ne savez pas à qui appartient le code, ni le vôtre, ni l'API.
Chris Wohlert

9
Seulement quatre tests? Vous avez six âges limites que vous devriez tester: 17, 18, 19, 20, 21, 22 ...;)
David Arno

22
@ FilipBartuzi, je suppose que la méthode vérifie si un homme a plus de 21 ans, par exemple? Dans son libellé actuel, il ne fait pas cela, ce n’est vrai que s’ils ont 22 ans ou plus. "Over 21" en anglais signifie "21+". Il y a donc un bug dans votre code. Ces bogues sont capturés en testant les valeurs limites, à savoir 20, 21, 22 pour un homme, 17,18,19 pour une femme dans ce cas. Donc, au moins six tests sont nécessaires.
David Arno

6
Sans parler des cas de 0 et -1. Qu'est-ce que cela signifie pour une personne d'avoir -1 ans? Que doit faire votre code si votre API renvoie une chose absurde?
RubberDuck

9
Ce serait beaucoup plus facile à tester si vous passiez un objet personne en tant que paramètre.
JeffO

Réponses:


72

Non, les tests d'intégration ne doivent pas simplement dupliquer la couverture des tests unitaires. Ils peuvent dupliquer une couverture, mais ce n'est pas le point.

Le but du test unitaire est de s'assurer qu'un petit fragment de fonctionnalité fonctionne exactement et complètement comme prévu. Un test unitaire pour am_i_old_enoughtester des données avec des âges différents, certainement ceux proches du seuil, peut-être tous les âges humains. Après avoir rédigé ce test, l'intégrité de am_i_old_enoughne devrait plus jamais être remise en question.

Le but d'un test d'intégration est de vérifier que l'ensemble du système, ou une combinaison d'un nombre important de composants, fonctionne correctement lorsqu'il est utilisé ensemble . Le client ne s’intéresse pas à une fonction d’utilité particulière que vous avez écrite, il tient à ce que son application Web soit correctement sécurisée contre l’accès des mineurs, sinon les régulateurs auront leur pareil.

La vérification de l'âge de l'utilisateur est une petite partie de cette fonctionnalité, mais le test d'intégration ne vérifie pas si votre fonction utilitaire utilise la valeur de seuil correcte. Il vérifie si l'appelant prend la bonne décision en fonction de ce seuil, si la fonction de service public est appelée, si les autres conditions d'accès sont remplies, etc.

La raison pour laquelle nous avons besoin des deux types de tests est essentiellement due à l’explosion combinatoire de scénarios possibles pour le chemin à travers une base de code que l’exécution peut prendre. Si la fonction utilitaire a environ 100 entrées possibles et qu'il existe des centaines de fonctions utilitaires, alors vérifier que la bonne chose se passe dans tous les cas nécessiterait de très nombreux millions de tests élémentaires. En vérifiant simplement tous les cas dans de très petits domaines, puis en vérifiant les combinaisons courantes, pertinentes ou probables de ces domaines, tout en supposant que ces petits domaines sont déjà corrects, comme le prouvent les tests unitaires , nous pouvons obtenir une évaluation assez confiante du système. ce qu'il devrait, sans noyer dans des scénarios alternatifs à tester.


6
"nous pouvons avoir une évaluation assez confiante du fait que le système fait ce qu'il devrait, sans se noyer dans des scénarios alternatifs à tester." Merci. J'aime quand quelqu'un aborde les tests automatisés avec bon sens.
jpmc26

1
JB Rainsberger a parlé des tests et de l'explosion combinatoire dont vous parlez dans le dernier paragraphe, intitulée "Les tests intégrés sont une arnaque" . Ce n'est pas tellement une question de tests d'intégration, mais c'est quand même assez intéressant.
Bart van Nierop

The customer doesn't care about a particular utility function you wrote, they care that their web app is properly secured against access by minors-> C'est très intelligent, merci! Le problème, c'est quand vous projetez vous-même. Difficile de séparer son état d'esprit entre être programmeur et chef de produit au même moment
Filip Bartuzi

14

La réponse courte est non". La partie la plus intéressante est pourquoi / comment cette situation pourrait survenir.

Je pense que la confusion survient parce que vous essayez de vous conformer à des pratiques de test strictes (tests unitaires par rapport à des tests d'intégration, moquages, etc.) pour un code qui ne semble pas adhérer à des pratiques strictes.

Cela ne veut pas dire que le code est "faux", ou que des pratiques particulières sont meilleures que d'autres. Simplement que certaines des hypothèses formulées par les pratiques de test peuvent ne pas s'appliquer dans cette situation, et il peut être utile d'utiliser un niveau similaire de "rigueur" dans les pratiques de codage et les pratiques de test; ou du moins, reconnaître qu'ils peuvent être déséquilibrés, ce qui rendra certains aspects inapplicables ou redondants.

La raison la plus évidente est que votre fonction effectue deux tâches différentes:

  • En levant les yeux en Personfonction de leur nom. Cela nécessite des tests d’intégration, pour s’assurer qu’il peut trouverPerson objets qui sont supposés être créés / stockés ailleurs.
  • Calculer si un Personest assez âgé, en fonction de son sexe. Cela nécessite des tests unitaires pour s'assurer que le calcul fonctionne comme prévu.

En regroupant ces tâches dans un bloc de code, vous ne pouvez en exécuter aucune sans l'autre. Lorsque vous souhaitez effectuer des tests unitaires, vous êtes obligé de rechercher un Personfichier (depuis une base de données réelle ou depuis un talon ou une maquette). Lorsque vous souhaitez vérifier que la recherche s'intègre au reste du système, vous devez également effectuer un calcul sur l'âge. Que devrions-nous faire avec ce calcul? Devons-nous l'ignorer ou le vérifier? Cela semble être la situation exacte que vous décrivez dans votre question.

Si nous imaginons une alternative, nous pourrions avoir le calcul seul:

def is_old_enough?(person)
   if person.male?
      return person.age > 21
   else 
      return person.age > 18
   end
end

S'agissant d'un calcul pur, nous n'avons pas besoin d'effectuer de test d'intégration.

Nous pourrions également être tentés d'écrire la tâche de recherche séparément:

def person_from_name(name = 'filip')
   return Person::API.new(name)
end

Cependant, dans ce cas, la fonctionnalité est si proche de Person::API.newcelle que je dirais que vous devriez plutôt l'utiliser (si le nom par défaut est nécessaire, serait-il préférable de le stocker ailleurs, comme un attribut de classe?).

Lorsque vous écrivez des tests d'intégration pour Person::API.new(ou person_from_name), tout ce dont vous avez besoin est de savoir si vous récupérez les résultats attendus Person; tous les calculs basés sur l'âge sont pris en charge ailleurs, de sorte que vos tests d'intégration peuvent les ignorer.


11

Un autre point que j'aime ajouter à la réponse de Killian est que les tests unitaires fonctionnent très rapidement, de sorte que nous pouvons en avoir plusieurs milliers. Un test d'intégration prend généralement plus de temps car il appelle des services Web, des bases de données ou une autre dépendance externe. Par conséquent, nous ne pouvons pas exécuter les mêmes tests (1 000) pour les scénarios d'intégration, car ils prendraient trop de temps.

En outre, les tests unitaires s'exécutent généralement à la construction du temps (sur la machine de construction) et les tests d'intégration courent après le déploiement sur un environnement / machine.

En règle générale, nous exécutons nos 1 000 tests unitaires pour chaque construction, puis nos quelque 100 tests d'intégration de grande valeur après chaque déploiement. Il est possible que nous ne prenions pas chaque génération pour le déploiement, mais cela n’est pas grave, car la génération que nous prenons pour déployer les tests d’intégration sera exécutée. En règle générale, nous souhaitons limiter l'exécution de ces tests dans un délai de 10 à 15 minutes, car nous ne voulons pas retarder le déploiement.

De plus, chaque semaine, nous pouvons exécuter une suite de tests d'intégration de régression couvrant plus de scénarios le week-end ou d'autres périodes d'indisponibilité. Celles-ci peuvent prendre plus de 15 minutes, car davantage de scénarios sont couverts, mais généralement personne ne travaille le samedi ou le dimanche, nous pouvons donc prendre plus de temps avec les tests.


ne s'applique pas aux langages dynamiques (c'est-à-dire sans l'étape de construction)
Filip Bartuzi
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.