web-dev-qa-db-fra.com

Les tests d'intégration (de base de données) sont-ils mauvais?

Certaines personnes maintiennent que les tests d'intégration sont toutes sortes de mauvais et de mauvais - tout doit être testé à l'unité, ce qui signifie que vous devez vous moquer des dépendances; une option que, pour diverses raisons, je n'aime pas toujours.

Je trouve que, dans certains cas, un test unitaire ne prouve tout simplement rien.

Prenons l'exemple d'implémentation du référentiel (trivial, naïf) (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]);
    }
}

Disons que je veux prouver dans un test que ce référentiel peut réellement trouver des produits correspondant à divers mots clés donnés.

À moins de tests d'intégration avec un véritable objet de connexion, 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 me moquer de l'objet de connexion dans un test unitaire, je ne peux que prouver des choses comme "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 à laquelle je m'attendais, mais peut-être que cette requête ne fait pas ce que je pense qu'elle fait.

En d'autres termes, je me sens comme un test qui fait des assertions sur la requête générée, est essentiellement sans valeur, car il teste la façon dont la méthode findByKeyword() 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ù faire des affirmations sur l'utilisation d'une maquette (test-double) ne fait que prouver comment les choses sont implémentées, pas si elles vont 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 vaut mieux tester une chose, et je comprends aussi pourquoi les tests d'intégration conduisent à 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 vraiment tester quoi que ce soit sans test d'intégration?

128
mindplay.dk

Votre collègue a raison de dire que tout ce qui peut être testé à l'unité doit l'être, et vous avez raison de dire que les tests unitaires ne vous mèneront que loin et pas plus loin, en particulier lors de l'écriture de wrappers simples autour de services externes complexes.

Une manière courante de penser au test est une pyramide de test . C'est un concept fréquemment lié à Agile, et beaucoup l'ont écrit, y compris Martin Fowler (qui l'attribue à Mike Cohn dans Réussir avec Agile ), Alistair Scott , et Google Testing Blog .

        /\                           --------------
       /  \        UI / End-to-End    \          /
      /----\                           \--------/
     /      \     Integration/System    \      /
    /--------\                           \----/
   /          \          Unit             \  /
  --------------                           \/
  Pyramid (good)                   Ice cream cone (bad)

L'idée est que les tests unitaires résilients et rapides sont la base du processus de test - il devrait y avoir des tests unitaires plus ciblés que les tests système/d'intégration, et plus de tests système/d'intégration que les tests de bout en bout. À mesure que vous vous rapprochez du sommet, les tests ont tendance à prendre plus de temps/ressources à exécuter, ont tendance à être plus fragiles et fragiles, et sont moins spécifiques pour identifier le système ou le fichier cassé ; naturellement, il est préférable d'éviter d'être "lourd".

À ce stade, les tests d'intégration ne sont pas mauvais , mais une forte dépendance à ceux-ci peut indiquer que vous n'avez pas conçu vos composants individuels pour être faciles à tester . Rappelez-vous, le but ici est de tester que votre unité fonctionne selon ses spécifications tout en impliquant un minimum d'autres systèmes cassables : Vous voudrez peut-être essayer une entrée -mémoire de base de données (que je considère comme un double test convivial pour les tests unitaires aux côtés des simulations) pour les tests de cas Edge lourds, 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 lorsque le système est assemblé.


En guise de remarque, vous avez mentionné que les simulations que vous écrivez testent simplement comment quelque chose est implémenté, pas si cela fonctionne . C'est quelque chose d'un contre-modèle: un test qui est un parfait miroir de sa mise en œuvre ne teste vraiment rien du tout. Au lieu de cela, testez que chaque classe ou méthode se comporte selon ses propres spécifications , quel que soit le niveau d'abstraction ou de réalisme requis.

134
Jeff Bowman

Un de mes collègues soutient que les tests d'intégration sont toutes sortes de mauvais et mauvais - tout doit être testé à l'unité,

C'est un peu comme dire que les antibiotiques sont mauvais - tout doit être soigné avec des vitamines.

Les tests unitaires ne peuvent pas tout attraper - 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 choses qui peuvent être testées indépendamment, et des tests d'intégration pour s'assurer que tout fonctionne ensemble.

À moins de tests d'intégration avec un véritable objet de connexion, 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?

Vous pourriez tester l'unité au niveau de la base de données. Exécutez la requête avec différents paramètres et voyez si vous obtenez les résultats attendus. Certes, cela signifie copier/coller toutes les modifications dans le "vrai" code. mais cela fait vous permet de tester la requête indépendamment de toute autre dépendance.

89
D Stanley

Les tests unitaires ne détectent pas tous les défauts. Mais ils sont moins chers à installer et à (re) exécuter que d'autres types de tests. Les tests unitaires sont justifiés par la combinaison d'une valeur modérée et d'un coût faible à modéré.

Voici un tableau montrant les taux de détection des défauts pour différents types de tests.

enter image description here

source: p.470 dans Code Complete 2 par McConnell

17
Nick Alexeev

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 la génération 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 voulons 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 avoir beaucoup de tests qui s'exécutent très rapidement.

Donc, pour une base de données, on devrait avoir quelque chose comme:

IRespository

List<Product> GetProducts<String Size, String Color);

Maintenant, la véritable implémentation d'IRepository ira à la base de données pour obtenir les produits, mais pour les tests unitaires, on peut se moquer d'IRepository avec un faux pour exécuter tous les tests selon les besoins sans base de données actaul car nous pouvons simuler toutes sortes de listes de produits en cours de retour 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 frontière. Nous voulons exécuter ces tests sur le serveur de déploiement (l'environnement réel), le bac à sable, ou même localement (pointé vers le bac à sable). Ils ne sont pas exécutés sur le serveur de génération. Une fois le logiciel déployé dans l'environnement, ceux-ci sont généralement exécutés en tant qu'activité de 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 voulons invoquer. Ceux-ci appellent en fait le vrai référentiel avec l'appel de base de données réel. Ces types de tests aident à:

  • Environnement Santé Stabilité Préparation
  • Tester la vraie chose

Ces tests sont parfois plus difficiles à exécuter car nous pouvons également avoir besoin de les configurer et/ou de les démonter. Pensez à ajouter un produit. Nous voulons probablement ajouter le produit, l'interroger pour voir s'il a été ajouté, puis après avoir terminé, le supprimer. Nous ne voulons pas ajouter 100s ou 1000s de produits "d'intégration", donc une configuration supplémentaire est nécessaire.

Les tests d'intégration peuvent s'avérer très utiles pour valider un environnement et s'assurer que la vraie chose fonctionne.

Il faut avoir les deux.

  • Exécutez les tests unitaires pour chaque build.
  • Exécutez les tests d'intégration pour chaque déploiement.
14
Jon Raynor

Les tests d'intégration de base de données ne sont pas mauvais. Plus encore, ils sont nécessaires.

Vous avez probablement votre application divisée en couches, et c'est une bonne chose. Vous pouvez tester chaque couche isolément en se moquant des couches voisines, et c'est aussi une bonne chose. Mais peu importe le nombre de couches d'abstraction que vous créez, à un moment donné, il doit y avoir une couche qui fait le sale boulot - en fait, parler à la base de données. À moins que vous ne le testiez, vous ne le faites pas testez du tout. Si vous testez la couche n en vous moquant de la couche n-1 vous évaluez l'hypothèse que la couche n fonctionne à condition que couche n-1 fonctionne. Pour que cela fonctionne, vous devez en quelque sorte prouver que la couche 0 fonctionne.

Bien qu'en théorie, vous puissiez tester la base de données 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 données de test à la volée et de lui parler.

Conclusion

Quelle est la confiance dégagée par les tests unitaires de votre référentiel abstrait, Ethereal Object-Relational-Mapper, Generic Active Record, Persistance théorique couches, quand finalement votre SQL généré contient une erreur de syntaxe?

12
el.pescado

L'auteur du article de blog auquel vous faites référence se préoccupe principalement de la complexité potentielle qui peut résulter des tests intégrés (bien qu'il soit rédigé de manière très avisée et catégorique). Cependant, les tests intégrés ne sont pas nécessairement mauvais et certains sont en fait 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 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'un côté, si ce que vous essayez de tester ne dépend pas, ou peut être fait pour ne pas dépendre du tout, de la base de données, alors écrivez votre test de telle manière qu'il n'essaye même pas d'utiliser le base de données (il suffit de fournir des données fictives au besoin). Par exemple, si vous essayez de tester une logique d'authentification lors du service d'une page Web (par exemple), c'est probablement une bonne chose de le détacher complètement de la base de données (en supposant que vous ne comptez pas sur la base de données pour l'authentification, ou que vous pouvez vous en moquer assez facilement).

D'un autre côté, s'il s'agit d'une fonctionnalité qui dépend directement de votre base de données et qui ne fonctionnerait pas du tout dans un environnement réel si la base de données n'était pas disponible, se moquer de ce que fait la base de données dans votre code client DB (c'est-à-dire la couche qui l'utilise) 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 souvent du comportement de la base de données sera une perte de temps. Les moteurs de base de données (en particulier le SGBDR) sont des systèmes complexes. Quelques lignes de SQL peuvent réellement effectuer beaucoup de travail, ce qui serait difficile à simuler (en fait, si votre requête SQL est longue de quelques lignes, 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): la duplication de la logique que vous avez déjà implémentée dans la base de données n'a pas de sens, et vérifier que le code de test deviendrait alors un problème en soi.

Je ne traiterais pas nécessairement cela comme un problème de test unitaire vs. test intégré , mais regardez plutôt la portée de ce qui est testé. Les problèmes globaux des tests unitaires et d'intégration demeurent: vous avez besoin d'un ensemble raisonnablement réaliste de données de test et de cas de test, mais quelque chose qui est également suffisamment petit pour que les tests soient exécutés rapidement.

Le temps de réinitialiser la base de données et de repeupler avec les données de test est un aspect à considérer; vous évalueriez généralement cela par rapport au temps qu'il faut pour écrire ce code simulé (que vous devrez également maintenir, éventuellement).

Un autre point à considérer est le degré de dépendance de votre application avec la base de données.

  • Si votre application suit simplement un modèle CRUD, où vous avez une couche d'abstraction qui vous permet de basculer entre n'importe quel SGBDR par le biais d'un simple paramètre de configuration, il y a de fortes chances que vous puissiez travailler avec un système simulé assez facilement (peut-être flou) la ligne entre les tests unitaires et intégrés à l'aide d'un SGBDR en mémoire).
  • Si votre application utilise une logique plus complexe, quelque chose qui serait spécifique à l'un de SQL Server, MySQL, PostgreSQL (par exemple), il serait généralement plus logique d'avoir un test qui utilise ce système spécifique.
7
Bruno

Vous avez besoin des deux.

Dans votre exemple, si vous testiez qu'une base de données dans une certaine condition, lorsque la méthode findByKeyword est exécutée, vous récupérez les données que vous attendez, il s'agit d'un test d'intégration fin.

Dans tout autre code qui utilise cette méthode findByKeyword, vous voulez contrôler ce qui est introduit dans le test, afin que vous puissiez retourner des valeurs nulles ou les bons mots pour votre test ou quoi que ce soit, puis vous vous moquez de la dépendance de la base de données afin que vous savoir exactement ce que votre test recevra (et vous perdez les frais généraux liés à la connexion à une base de données et à la garantie que les données à l'intérieur sont correctes)

6
Froome

Vous avez raison de penser qu'un tel test unitaire est incomplet. L'incomplétude est dans l'interface de base de données en cours de moquerie. Ces attentes ou assertions naïves de cette maquette sont incomplètes.

Pour le terminer, vous devrez épargner suffisamment de temps et de ressources pour écrire ou intégrer un moteur de règles SQL qui garantirait cette instruction SQL émise par sujet à l’essai entraînerait les opérations prévues.

Cependant, l'alternative/compagnon souvent oubliée et quelque peu coûteuse de la moquerie est "virtualisation".

Pouvez-vous faire tourner une instance de base de données temporaire mais 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. Les points de vue varient quant à l'endroit où tracer la ligne de classement entre les tests unitaires et les tests d'intégration. À mon humble avis, "unité" est une définition arbitraire et devrait répondre à vos besoins.

1
S.D.

Dans le monde .Net, j'ai l'habitude de créer un projet de test et de créer des tests comme 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é à exécuter tous les tests pour chaque génération (car cela ralentit mon flux de travail de développement), mais je comprends l'utilité de cela pour une équipe plus grande. Néanmoins, vous pouvez établir une règle selon laquelle, avant de valider le code, tous les tests doivent être exécutés et réussis (si cela prend plus de temps pour que les tests s'exécutent car la base de données est réellement atteinte).

Se moquer de la couche d'accès aux données (DAO) et ne pas réellement toucher la base de données, non seulement ne me permet pas de coder comme je le souhaite et auquel je suis habitué, mais il manque une grande partie 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 que vous faites simplement semblant, puis que vous passez beaucoup de temps à simuler des choses, je n'arrive pas à comprendre l'utilité de cette approche pour tester réellement mon code. Je teste un petit morceau au lieu d'un plus gros avec un test. Je comprends que mon approche ressemble davantage à un test d'intégration, mais il semble que le test unitaire avec la maquette soit une perte de temps redondante si vous écrivez simplement le test d'intégration une fois et d'abord. C’est aussi un bon moyen de développer et de déboguer.

En fait, depuis un certain temps maintenant, je suis au courant du TDD et du Behaviour Driven Design (BDD) et je réfléchis aux moyens de l’utiliser, mais il est difficile d’ajouter des tests unitaires rétroactivement. Je me trompe peut-être, mais l'écriture d'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 de priorité plus élevée pour écrire qui couvre plus de code et est un moyen plus efficace d'écrire des tests.

En fait, je pense que quelque chose comme Behavior Driven Design (BDD) qui tente de tester de bout en bout avec un langage spécifique au domaine (DSL) devrait être la voie à suivre. Nous avons SpecFlow dans le monde .Net, mais il a commencé comme open source avec Cucumber.

https://cucumber.io/

Je ne suis vraiment pas impressionné par la véritable utilité du test que j'ai écrit en se moquant de la couche d'accès aux données et en ne frappant pas la base de données. L'objet retourné n'a pas atteint 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 d'une manière contre nature. Je pense juste que c'est une perte de temps.

Selon Stack Overflow, la moquerie est utilisée lorsque des objets réels ne peuvent pas être incorporés dans le test unitaire.

https://stackoverflow.com/questions/2665812/what-is-mocking

"La simulation est principalement utilisée dans les tests unitaires. Un objet testé peut avoir des dépendances avec d'autres objets (complexes). Pour isoler le comportement de l'objet que vous souhaitez tester, vous remplacez les autres objets par des simulations simulant le comportement des objets réels. Cela est utile si les objets réels ne peuvent pas être incorporés dans le test unitaire. "

Mon argument est que si je code quelque chose de bout en bout (interface utilisateur Web, couche métier, couche d'accès aux données, base de données, aller-retour), avant de vérifier quoi que ce soit en tant que développeur, je vais tester ce flux aller-retour. Si je coupe l'interface utilisateur et que je débogue et teste ce flux à partir d'un test, je teste tout ce qui est court par rapport à l'interface utilisateur et je retourne exactement ce que l'interface utilisateur 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, cela devrait être le test de priorité le plus élevé qui couvre le test de bout en bout des spécifications réelles de l'utilisateur autant que possible. Si je ne crée jamais d'autres tests plus granulaires, au moins j'ai ce test plus complet qui prouve que mes fonctionnalités souhaitées fonctionnent.

Un co-fondateur de Stack Exchange n'est pas convaincu des avantages d'avoir une couverture de test unitaire à 100%. Moi non plus. Je prendrais un "test d'intégration" plus complet qui atteindrait la base de données au lieu de maintenir un tas de simulations de base de données tous les jours.

https://www.joelonsoftware.com/2009/01/31/from-podcast-38/

0
user3198764

Unit Tests et Integration Tests sont orthogonaux entre eux. Ils offrent une vue différente sur l'application que vous créez. Habituellement vous voulez les deux. Mais le moment diffère, quand vous voulez quel genre de tests.

Le plus souvent, vous voulez Unit Tests. Les tests unitaires se concentrent sur une petite partie du code testé - ce qui s'appelle exactement un unit est laissé au lecteur. Mais le but est simple: obtenir un retour rapide de lorsque et votre code s'est cassé . Cela dit, il doit être clair que les appels vers une base de données réelle sont un nono .

D'un autre côté, il y a des choses qui ne peuvent être testées que par unité dans des conditions difficiles sans base de données. Il existe peut-être une condition de concurrence dans votre code et un appel à une base de données déclenche une violation d'un unique constraint qui ne peut être levé 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 que unit tests.

0
Thomas Junk