Certaines personnes soutiennent que les tests d'intégration sont des types de tests incorrects et erronés - tout doit être testé en unité, ce qui signifie que vous devez simuler des dépendances. une option qui, pour diverses raisons, ne me passionne pas toujours.
Je trouve que, dans certains cas, un test unitaire ne prouve simplement rien.
Prenons l'exemple de l'implémentation de référentiel (trivial, naïf) suivant (en PHP):
class ProductRepository
{
private $db;
public function __construct(ConnectionInterface $db) {
$this->db = $db;
}
public function findByKeyword($keyword) {
// this might have a query builder, keyword processing, etc. - this is
// a totally naive example just to illustrate the DB dependency, mkay?
return $this->db->fetch("SELECT * FROM products p"
. " WHERE p.name LIKE :keyword", ['keyword' => $keyword]);
}
}
Supposons que je souhaite prouver par un test que ce référentiel peut réellement trouver des produits correspondant à différents mots clés donnés.
En dehors des tests d'intégration avec un objet de connexion réel, comment puis-je savoir que cela génère en fait de vraies requêtes - et que ces requêtes font réellement ce que je pense qu'elles font?
Si je dois simuler l'objet de connexion dans un test unitaire, je ne peux que prouver que "il génère la requête attendue" - mais cela ne signifie pas qu'il va réellement fonctionner ... c'est-à-dire qu'il génère peut-être la requête. Je m'y attendais, mais peut-être que cette requête ne fait pas ce que je pense.
En d'autres termes, j'ai l'impression qu'un test qui émet des assertions sur la requête générée est essentiellement sans valeur, car il teste la manière dont la findByKeyword()
méthode a été implémentée , mais cela ne prouve pas que cela fonctionne réellement .
Ce problème ne se limite pas aux référentiels ou à l’intégration de bases de données - il semble s’appliquer dans de nombreux cas, où affirmer l’utilisation d’un simulacre de test (double test) ne fait que prouver comment les choses sont mises en œuvre, et non pas si elles vont être utilisées. fonctionne réellement.
Comment gérez-vous de telles situations?
Les tests d'intégration sont-ils vraiment "mauvais" dans un cas comme celui-ci?
Je comprends qu'il est préférable de tester une chose, et je comprends aussi pourquoi les tests d'intégration mènent à une myriade de chemins de code, qui ne peuvent pas tous être testés - mais dans le cas d'un service (tel qu'un référentiel) dont le seul but est Pour interagir avec un autre composant, comment pouvez-vous réellement tester quoi que ce soit sans tester l'intégration?
la source
Réponses:
Votre collègue a raison de dire que tout ce qui peut être testé en unité devrait l'être et que les tests unitaires ne vous mèneront que très loin, en particulier lorsque vous écrivez de simples wrappers autour de services externes complexes.
Une façon courante de penser aux tests consiste à utiliser une pyramide de tests . C'est un concept fréquemment associé à Agile, et beaucoup l'ont écrit, y compris Martin Fowler (qui l'attribue à Mike Cohn dans Succeeding with Agile ), Alistair Scott et le blog Google Testing .
L'idée est que des tests unitaires rapides et résilients constituent le fondement du processus de test: il devrait exister davantage de tests unitaires ciblés que de tests système / d'intégration et davantage de tests système / d'intégration que de tests de bout en bout. Au fur et à mesure que vous vous rapprochez du sommet, les tests prennent plus de temps / ressources à exécuter, ont tendance à être plus fragiles et plus fragiles, et sont moins spécifiques dans l'identification du système ou du fichier cassé ; naturellement, il est préférable d'éviter d'être trop lourd.
À ce stade, les tests d'intégration ne sont pas mauvais , mais une forte confiance en eux peut indiquer que vous n'avez pas conçu vos composants individuels pour qu'ils soient faciles à tester. Rappelez-vous que le but ici est de vérifier que votre appareil respecte ses spécifications tout en utilisant au minimum d’autres systèmes fragiles : Vous pouvez essayer une base de données en mémoire (que je considère comme un test à l’unité permettant de tester facilement les unités) ) pour des tests de cas extrêmes, par exemple, puis écrivez quelques tests d'intégration avec le moteur de base de données réel pour établir que les cas principaux fonctionnent lors de l'assemblage du système.
En remarque, vous avez mentionné que les simulacres que vous écrivez testent simplement comment quelque chose est implémenté, pas si cela fonctionne . C'est un peu un anti-modèle: un test qui reflète parfaitement son implémentation ne teste pas vraiment quoi que ce soit. Au lieu de cela, vérifiez que chaque classe ou méthode se comporte conformément à ses propres spécifications , quel que soit le niveau d'abstraction ou de réalisme requis.
la source
C'est un peu comme dire que les antibiotiques sont mauvais - tout devrait être soigné avec des vitamines.
Les tests unitaires ne peuvent pas tout comprendre: ils testent uniquement le fonctionnement d'un composant dans un environnement contrôlé . Les tests d'intégration vérifient que tout fonctionne ensemble , ce qui est plus difficile à faire mais plus significatif à la fin.
Un bon processus de test complet utilise les deux types de tests: des tests unitaires pour vérifier les règles métier et d'autres éléments pouvant être testés indépendamment, et des tests d'intégration pour s'assurer que tout fonctionne bien.
Vous pouvez le tester à l' unité au niveau de la base de données . Exécutez la requête avec divers paramètres et voyez si vous obtenez les résultats escomptés. Accordé signifie copier / coller toute modification dans le "vrai" code. mais il ne vous permet de tester la requête indépendante de toutes les autres dépendances.
la source
Les tests unitaires ne détectent pas tous les défauts. Mais ils sont moins chers à installer et à (ré) exécuter par rapport à d’autres types de tests. Les tests unitaires sont justifiés par une combinaison de valeur modérée et de coût faible à modéré.
Voici un tableau indiquant les taux de détection des défauts pour différents types de tests.
source: p.470 dans Code Complete 2 par McConnell
la source
Non, ils ne sont pas mauvais. Espérons que l'on devrait avoir des tests unitaires et d'intégration. Ils sont utilisés et exécutés à différentes étapes du cycle de développement.
Tests unitaires
Les tests unitaires doivent être exécutés sur le serveur de génération et localement, une fois le code compilé. Si des tests unitaires échouent, il faut échouer lors de la construction ou ne pas valider la mise à jour du code jusqu'à ce que les tests soient corrigés. La raison pour laquelle nous voulons que les tests unitaires soient isolés est que nous souhaitons que le serveur de build puisse exécuter tous les tests sans toutes les dépendances. Ensuite, nous pourrions exécuter la construction sans toutes les dépendances complexes requises et effectuer un grand nombre de tests très rapides.
Donc, pour une base de données, on devrait avoir quelque chose comme:
Maintenant, l'implémentation réelle d'IRepository ira à la base de données pour obtenir les produits, mais pour les tests unitaires, on peut simuler IRepository avec un faux pour exécuter tous les tests nécessaires sans base de données actaul, car nous pouvons simuler toutes sortes de listes de produits. renvoyé à partir de l'instance fictive et testez toute logique métier avec les données fictives.
Tests d'intégration
Les tests d'intégration sont généralement des tests de franchissement de limites. Nous voulons exécuter ces tests sur le serveur de déploiement (environnement réel), le bac à sable ou même localement (pointé sur le bac à sable). Ils ne sont pas exécutés sur le serveur de compilation. Une fois que le logiciel a été déployé dans l'environnement, ceux-ci sont généralement exécutés comme une activité post-déploiement. Ils peuvent être automatisés via des utilitaires de ligne de commande. Par exemple, nous pouvons exécuter nUnit à partir de la ligne de commande si nous catégorisons tous les tests d'intégration que nous souhaitons appeler. Celles-ci appellent en réalité le référentiel réel avec l'appel de la base de données réelle. Ce type de test aide à:
Ces tests sont parfois plus difficiles à exécuter car nous pouvons avoir besoin de les installer et / ou de les démolir également. Pensez à ajouter un produit. Nous souhaitons probablement ajouter le produit, l'interroger pour savoir s'il a été ajouté, puis le supprimer une fois l'opération terminée. Nous ne souhaitons pas ajouter 100 ou 1 000 produits "d'intégration", une configuration supplémentaire est donc nécessaire.
Les tests d'intégration peuvent s'avérer très utiles pour valider un environnement et s'assurer que la réalité fonctionne.
On devrait avoir les deux.
la source
Les tests d'intégration de base de données ne sont pas mauvais. Encore plus, ils sont nécessaires.
Votre application est probablement divisée en couches, et c'est une bonne chose. Vous pouvez tester chaque couche séparément en vous moquant des couches voisines, ce qui est également une bonne chose. Mais quel que soit le nombre de couches d'abstraction que vous créez, il doit y avoir à un moment donné une couche qui fait le sale boulot - parler en fait à la base de données. À moins de le tester, vous ne le testez pas du tout. Si vous testez la couche n en vous moquant de la couche n-1, vous évaluez l'hypothèse selon laquelle la couche n fonctionne à condition que la couche n-1 fonctionne. Pour que cela fonctionne, vous devez prouver que la couche 0 fonctionne.
Tandis qu'en théorie, vous pourriez utiliser la base de données de test unitaire, en analysant et en interprétant le SQL généré, il est beaucoup plus facile et plus fiable de créer une base de test à la volée et d’en parler.
Conclusion
Quelle est la confiance garantie par les tests unitaires des couches référentiel abstrait , mappeur d'objet relationnel Ethereal , enregistrement actif générique , persistance théorique , lorsque, finalement, votre code SQL généré contient une erreur de syntaxe?
la source
Vous avez besoin des deux.
Dans votre exemple, si vous testiez une base de données dans certaines conditions, lorsque la
findByKeyword
méthode est exécutée, vous récupérez les données qui devraient, selon vous, constituer un test d'intégration fin.Dans tout autre code utilisant cette
findByKeyword
méthode, vous souhaitez contrôler le contenu du test. Vous pouvez ainsi renvoyer les valeurs NULL ou les mots corrects pour votre test, ou tout ce que vous ferez de la dépendance à la base de données, de sorte que vous sachiez exactement ce que sera votre test. recevez (et vous perdez la surcharge de vous connecter à une base de données et de vous assurer que les données sont correctes)la source
L'auteur de l' article de blog auquel vous faites référence est principalement concerné par la complexité potentielle pouvant résulter de tests intégrés (même s'il est écrit de manière très catégorique et basée sur l'opinion). Cependant, les tests intégrés ne sont pas nécessairement mauvais et certains sont en réalité plus utiles que les tests unitaires purs. Cela dépend vraiment du contexte de votre application et de ce que vous essayez de tester.
De nos jours, de nombreuses applications ne fonctionneraient tout simplement pas du tout si leur serveur de base de données tombait en panne. Au moins, pensez-y dans le contexte de la fonctionnalité que vous essayez de tester.
D’une part, si ce que vous essayez de tester ne dépend pas ou ne peut en aucune manière dépendre de la base de données, écrivez votre test de telle sorte qu’il n’essaye même pas d’utiliser la base de données (juste fournir des données factices si nécessaire). Par exemple, si vous essayez de tester une logique d’authentification lorsque vous servez une page Web (par exemple), il est probablement préférable de la dissocier de la base de données (en supposant que vous ne vous fiez pas à la base de données pour l’authentification. vous pouvez vous moquer assez facilement).
D'autre part, s'il s'agit d'une fonctionnalité qui repose directement sur votre base de données et qui ne fonctionnerait pas du tout dans un environnement réel si la base de données était indisponible, alors vous vous moquez de ce que fait la base de données dans votre code client de base de données (c'est-à-dire DB) n'a pas nécessairement de sens.
Par exemple, si vous savez que votre application va s'appuyer sur une base de données (et éventuellement sur un système de base de données spécifique), se moquer du comportement de la base de données pour le plaisir sera souvent une perte de temps. Les moteurs de base de données (en particulier les SGBDR) sont des systèmes complexes. Quelques lignes de SQL peuvent en réalité effectuer beaucoup de travail, ce qui serait difficile à simuler (en fait, si votre requête SQL est longue, il est probable que vous aurez besoin de beaucoup plus de lignes de Java / PHP / C # / Python. code pour produire le même résultat en interne): dupliquer la logique que vous avez déjà implémentée dans la base de données n’a aucun sens, et vérifier que le code de test deviendrait alors un problème en soi.
Je ne considérerais pas nécessairement cela comme un problème de test unitaire par rapport à un test intégré , mais plutôt d'examiner l'étendue de ce qui est testé. Les problèmes généraux liés aux tests unitaires et d'intégration subsistent: vous avez besoin d'un ensemble raisonnablement réaliste de données de test et de scénarios de test, mais aussi d'une taille suffisamment petite pour que les tests puissent être exécutés rapidement.
Le temps nécessaire pour réinitialiser la base de données et repeupler avec les données de test est un aspect à prendre en compte; vous évalueriez généralement ceci par rapport au temps nécessaire pour écrire ce code factice (que vous auriez éventuellement à maintenir aussi).
Un autre point à considérer est le degré de dépendance de votre application avec la base de données.
la source
Vous avez raison de penser qu'un tel test unitaire est incomplet. Le caractère incomplet se trouve dans l'interface de base de données en cours de simulation. L'attente ou les affirmations d'une telle maquette naïve sont incomplètes.
Pour le rendre complet, vous devez disposer de suffisamment de temps et de ressources pour écrire ou intégrer un moteur de règles SQL garantissant que les instructions SQL émises par le sujet testé entraînent les opérations attendues.
Cependant, l'alternative / compagne de moquerie souvent oubliée et un peu chère est la "virtualisation" .
Pouvez-vous lancer une instance de base de données temporaire, en mémoire, mais "réelle" pour tester une seule fonction? Oui ? Là, vous avez un meilleur test, celui qui vérifie les données réelles enregistrées et récupérées.
Maintenant, on pourrait dire que vous avez transformé un test unitaire en test d’intégration. Il existe différents points de vue sur les points sur lesquels tracer la ligne à classer entre les tests unitaires et les tests d'intégration. IMHO, "unité" est une définition arbitraire et devrait correspondre à vos besoins.
la source
Unit Tests
etIntegration Tests
sont orthogonaux les uns aux autres. Ils offrent une vue différente de l'application que vous construisez. Habituellement, vous voulez les deux . Mais le moment diffère selon le type de test.Le plus souvent tu veux
Unit Tests
. Les tests unitaires se concentrent sur une petite partie du code testé - ce qui est appelé exactement ununit
est laissé au lecteur. Mais le but est simple: obtenir un retour rapide sur le moment et le lieu où votre code a été cassé . Cela dit, il devrait être clair que les appels vers une base de données réelle est un nono .D'autre part, il y a des choses qui ne peuvent être testées à l'unité que dans des conditions difficiles sans base de données. Votre code contient peut-être une condition de concurrence critique et un appel à une base de données renvoie une violation
unique constraint
qui ne peut être déclenchée que si vous utilisez réellement votre système. Mais ces types de tests sont coûteux, vous ne pouvez pas (et ne voulez pas) les exécuter aussi souvent queunit tests
.la source
Dans le monde .Net, j'ai l'habitude de créer un projet de test et de créer des tests en tant que méthode de codage / débogage / test aller-retour moins l'interface utilisateur. C'est un moyen efficace pour moi de me développer. Je n'étais pas aussi intéressé par l'exécution de tous les tests pour chaque version (car cela ralentit mon flux de travail de développement), mais je comprends son utilité pour une équipe plus grande. Néanmoins, vous pouvez faire savoir qu'avant de valider le code, tous les tests doivent être exécutés et passés (si l'exécution des tests est plus longue, car la base de données est en train d'être touchée).
Mocker la couche d'accès aux données (DAO) et ne pas frapper la base de données ne me permet pas non plus de coder comme je le souhaite et s'y est habitué, mais il manque un gros morceau de la base de code réelle. Si vous ne testez pas vraiment la couche d'accès aux données et la base de données et ne faites que simuler, puis passez beaucoup de temps à vous moquer, je ne parviens pas à saisir l'utilité de cette approche pour réellement tester mon code. Je teste un petit morceau au lieu d'un plus gros avec un test. Je comprends que mon approche ressemble peut-être davantage à un test d’intégration, mais il semble que le test unitaire avec la maquette est une perte de temps redondante si vous écrivez simplement le test d’intégration une fois pour la première fois. C'est aussi un bon moyen de développer et de déboguer.
En fait, je connais le TDD et la conception axée sur le comportement (BDD) et réfléchis depuis un moment à son utilisation, mais il est difficile d’ajouter des tests unitaires rétroactivement. Je me trompe peut-être, mais écrire un test qui couvre plus de code de bout en bout avec la base de données incluse semble être un test beaucoup plus complet et prioritaire à écrire qui couvre plus de code et constitue un moyen plus efficace d'écrire des tests.
En fait, je pense que quelque chose comme BDD (Behavior Driven Design) qui tente de tester de bout en bout un langage spécifique à un domaine (DSL) devrait être la solution. Nous avons SpecFlow dans le monde .Net, mais cela a commencé en open source avec Cucumber.
https://cucumber.io/
Je ne suis vraiment pas vraiment impressionné par la véritable utilité du test que j'ai écrit pour se moquer de la couche d'accès aux données et ne pas toucher à la base de données. L'objet renvoyé n'a pas touché la base de données et n'a pas été rempli de données. C'était un objet entièrement vide que je devais simuler de manière non naturelle. Je pense juste que c'est une perte de temps.
Selon Stack Overflow, le mocking est utilisé lorsqu'il est impossible d'incorporer des objets réels dans le test unitaire.
https://stackoverflow.com/questions/2665812/what-is-mocking
"Le mocking est principalement utilisé dans les tests unitaires. Un objet testé peut avoir des dépendances sur d'autres objets (complexes). Pour isoler le comportement de l'objet que vous souhaitez tester, remplacez les autres objets par des simulacres simulant le comportement des objets réels. Ceci est utile si les objets réels ne peuvent pas être incorporés au test unitaire. "
Mon argument est que, si je codifie quoi que ce soit bout à bout (interface utilisateur Web entre couche Web, couche d'accès aux données dans la base de données, aller-retour), avant d'enregistrer quoi que ce soit en tant que développeur, je vais tester ce flux aller-retour. Si je coupe l'interface utilisateur, que je débogue et teste ce flux à partir d'un test, je teste tout ce qui reste avant l'interface utilisateur et renvoie exactement ce à quoi l'interface utilisateur s'attend. Il ne me reste plus qu'à envoyer à l'interface utilisateur ce qu'elle veut.
J'ai un test plus complet qui fait partie de mon flux de travail de développement naturel. Pour moi, il devrait s'agir du test de priorité la plus élevée couvrant le test de bout en bout de la spécification de l'utilisateur. Si je ne crée jamais d'autres tests plus granulaires, au moins j'ai ce test plus complet qui prouve que la fonctionnalité souhaitée fonctionne.
Un cofondateur de Stack Exchange n'est pas convaincu des avantages d'une couverture de test unitaire à 100%. Moi aussi je ne suis pas. Je prendrais un "test d'intégration" plus complet qui frappe la base de données en conservant un tas de bases de données mock tous les jours.
https://www.joelonsoftware.com/2009/01/31/from-podcast-38/
la source
Les dépendances externes doivent être simulées car vous ne pouvez pas les contrôler (elles peuvent réussir lors de la phase de test d'intégration mais échouer en production). Les lecteurs peuvent échouer, les connexions à la base de données peuvent échouer pour un certain nombre de raisons, il peut y avoir des problèmes de réseau, etc.
Avec de vrais tests unitaires, vous testez dans les limites du bac à sable et cela devrait être clair. Si un développeur écrit une requête SQL qui a échoué dans QA / PROD, cela signifie qu'il ne l'a même pas testée une seule fois auparavant.
la source