Disons que j'ai une fonction (écrite en Ruby, mais qui devrait être compréhensible par tout le monde):
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
Dans les tests unitaires, je créerais quatre tests pour couvrir tous les scénarios. Chacun utilisera un objet Person::API
Simulé avec les méthodes tronquées male?
Et age
.
Il s'agit maintenant d'écrire des tests d'intégration. Je suppose que Person :: API ne devrait plus être moqué. 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 bon écrire des tests unitaires, si je pouvais simplement écrire des tests d'intégration qui me donnent plus de confiance (car je travaille sur des objets réels, pas sur des talons ou des maquettes)?
Non, les tests d'intégration devraient non seulement dupliquer la couverture des tests unitaires. Ils peuvent dupliquer une certaine couverture, mais ce n'est pas le point.
Le but d'un test unitaire est de s'assurer qu'un petit morceau de fonctionnalité spécifique fonctionne exactement et complètement comme prévu. Un test unitaire pour am_i_old_enough
testerait des données d'âges différents, certainement ceux proches du seuil, peut-être tous les âges humains survenus. Une fois ce test terminé, l'intégrité de am_i_old_enough
ne devrait plus jamais être remis en question.
Le point 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, fait la bonne chose lorsqu'ils sont utilisés ensemble. Le client ne se soucie pas d'une fonction d'utilité particulière que vous avez écrite, il se soucie que son application Web soit correctement sécurisée contre l'accès des mineurs, car sinon les régulateurs auront leur cul.
Vérifier l'âge de l'utilisateur est n petite partie de cette fonctionnalité, mais le test d'intégration ne vérifie pas si votre fonction d'utilité utilise la valeur de seuil correcte. Il teste si l'appelant prend la bonne décision en fonction de ce seuil, si la fonction d'utilité est appelée du tout, si les conditions d'accès other sont remplies, etc.
La raison pour laquelle nous avons besoin des deux types de tests est essentiellement qu'il existe une 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 y a des centaines de fonctions utilitaires, vérifier que la bonne chose se produit dans les cas tous nécessiterait plusieurs millions de cas de test. En vérifiant simplement tous les cas dans de très petites portées, puis en vérifiant les cas communs, pertinents ou probables combinaisons f ces portées, tout en supposant que ces petites portées sont déjà correctes, comme le montrent les tests unitaires =, nous pouvons obtenir une évaluation assez sûre que le système fait ce qu'il devrait, sans se noyer dans des scénarios alternatifs à tester.
La réponse courte est non". La partie la plus intéressante est pourquoi/comment cette situation pourrait se produire.
Je pense que la confusion vient du fait que vous essayez d'adhérer à des pratiques de test strictes (tests unitaires vs tests d'intégration, mocking, etc.) pour du code qui ne semble pas adhérer à des pratiques strictes.
Cela ne veut pas dire que le code est "mauvais" ou que certaines pratiques sont meilleures que d'autres. Simplement que certaines des hypothèses faites par les pratiques de test peuvent ne pas s'appliquer dans cette situation, et cela peut aider à utiliser un niveau similaire de "rigueur" dans les pratiques de codage et les pratiques de test; ou du moins, de reconnaître qu'ils pourraient être déséquilibrés, ce qui rendrait certains aspects inapplicables ou redondants.
La raison la plus évidente est que votre fonction exécute deux tâches différentes:
Person
en fonction de son nom. Cela nécessite des tests d'intégration, pour s'assurer qu'il peut trouver des objets Person
qui sont probablement créés/stockés ailleurs.Person
est assez vieux, 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 pas exécuter l'une sans l'autre. Lorsque vous souhaitez tester un par un les calculs, vous êtes obligé de rechercher un Person
(à partir d'une base de données réelle ou d'un stub/mock). Lorsque vous souhaitez tester que la recherche s'intègre avec le reste du système, vous êtes également obligé d'effectuer un calcul sur l'âge. Que devons-nous faire avec ce calcul? Faut-il 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
Puisqu'il s'agit d'un calcul pur, nous n'avons pas besoin d'effectuer de tests d'intégration dessus.
Nous pourrions également être tentés d'écrire séparément la tâche de recherche:
def person_from_name(name = 'filip')
return Person::API.new(name)
end
Cependant, dans ce cas, la fonctionnalité est si proche de Person::API.new
que je dirais que vous devriez utiliser cela à la place (si le nom par défaut est nécessaire, serait-il préférable de le stocker ailleurs, comme un attribut de classe?).
Lors de l'écriture de 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 le Person
attendu; tous les calculs basés sur l'âge sont pris en charge ailleurs, de sorte que vos tests d'intégration peuvent les ignorer.
Un autre point que j'aime ajouter à la réponse de Killian est que les tests unitaires s'exécutent très rapidement, donc nous pouvons en avoir des 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 d'autres dépendances externes, nous ne pouvons donc pas exécuter les mêmes tests (1000) pour les scénarios d'intégration car ils prendraient trop de temps.
En outre, les tests unitaires sont généralement exécutés à build heure (sur la machine de génération) et les tests d'intégration exécutés après déploiement sur un environnement/une machine.
En règle générale, on exécuterait nos milliers de tests unitaires pour chaque génération, puis nos 100 tests d'intégration à valeur élevée après chaque déploiement. Nous ne pouvons pas prendre chaque build pour le déploiement, mais c'est OK car la build que nous prenons pour déployer les tests d'intégration sera exécutée. En règle générale, nous voulons limiter ces tests à s'exécuter dans les 10 ou 15 minutes, car nous ne voulons pas retarder le déploiement trop longtemps.
De plus, sur une base hebdomadaire, nous pouvons exécuter une suite de tests de régression d'intégration qui couvrent plus de scénarios le week-end ou d'autres temps d'arrêt. Cela peut prendre plus de 15 minutes car plus de scénarios seront couverts, mais généralement personne ne travaille sur Sat/Sun, nous pouvons donc prendre plus de temps avec les tests.