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::API
objet 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)?
unit-testing
testing
ruby
integration-tests
Filip Bartuzi
la source
la source
Réponses:
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_enough
tester 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é deam_i_old_enough
ne 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.
la source
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 momentLa 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:
Person
fonction 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.Person
est 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
Person
fichier (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:
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:
Cependant, dans ce cas, la fonctionnalité est si proche de
Person::API.new
celle 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
(ouperson_from_name
), tout ce dont vous avez besoin est de savoir si vous récupérez les résultats attendusPerson
; tous les calculs basés sur l'âge sont pris en charge ailleurs, de sorte que vos tests d'intégration peuvent les ignorer.la source
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.
la source