web-dev-qa-db-fra.com

Tests unitaires et bases de données: à quel moment puis-je réellement me connecter à la base de données?

Il existe des réponses à la question sur la façon dont les classes de test qui se connectent à une base de données, par exemple "Les classes de test de service doivent-elles se connecter ..." et "Test unitaire - Application couplée à la base de données" .

Donc, en bref, supposons que vous avez une classe A qui doit se connecter à une base de données. Au lieu de laisser A se connecter, vous fournissez à A une interface que A peut utiliser pour se connecter. Pour les tests, vous implémentez cette interface avec certaines choses - sans vous connecter bien sûr. Si la classe B instancie A, elle doit passer une "vraie" connexion à la base de données à A. Mais cela signifie que B ouvre une connexion à la base de données. Cela signifie que pour tester B, vous injectez la connexion dans B. Mais B est instancié dans la classe C et ainsi de suite.

Alors, à quel moment dois-je dire "ici, je récupère les données d'une base de données et je n'écrirai pas de test unitaire pour ce morceau de code"?

En d'autres termes: quelque part dans le code d'une classe I doit appeler sqlDB.connect() ou quelque chose de similaire. Comment tester cette classe?

Et est-ce la même chose avec le code qui doit gérer une interface graphique ou un système de fichiers?


Je veux faire un test unitaire. Tout autre type de test n'est pas lié à ma question. Je sais que je ne testerai qu'une seule classe avec (je suis donc d'accord avec toi Kilian). Maintenant, une classe doit se connecter à une base de données. Si je veux tester cette classe et demander "Comment faire", beaucoup disent: "Utilisez l'injection de dépendance!" Mais cela ne fait que déplacer le problème dans une autre classe, n'est-ce pas? Alors je demande, comment puis-je tester la classe qui établit vraiment, vraiment la connexion?

Question bonus: certaines réponses ici se résument à "Utiliser des objets fictifs!" Qu'est-ce que ça veut dire? Je me moque des classes dont dépend la classe sous test. Dois-je me moquer de la classe en cours de test maintenant et tester réellement la maquette (qui se rapproche de l'idée d'utiliser des méthodes de modèle, voir ci-dessous)?

37
TobiMcNamobi

Le modèle de méthode de modèle pourrait aider.

Vous encapsulez les appels vers une base de données dans les méthodes protected. Pour tester cette classe, vous testez en fait un faux objet qui hérite de la vraie classe de connexion à la base de données et remplace les méthodes protégées.

De cette façon, les appels réels à la base de données ne sont jamais soumis à des tests unitaires, c'est vrai. Mais ce ne sont que ces quelques lignes de code. Et c'est acceptable.

1
TobiMcNamobi

Le point d'un test unitaire est de tester ne classe (en fait, il devrait généralement tester ne méthode).

Cela signifie que lorsque vous testez la classe A, vous y injectez une base de données de test - quelque chose d'auto-écrit, ou une base de données en mémoire ultra-rapide, quel que soit le travail effectué.

Cependant, si vous testez la classe B, qui est un client de A, vous vous moquez généralement de l'ensemble de l'objet A avec autre chose, vraisemblablement quelque chose qui fait son travail dans un manière primitive et préprogrammée - sans utiliser un objet A réel et certainement sans utiliser de base de données (à moins que A ne transmette l'intégralité de la connexion de la base de données à son appelant - mais c'est tellement horrible que je ne veux pas y penser). De même, lorsque vous écrivez un test unitaire pour la classe C, qui est un client de B, vous vous moquez de quelque chose qui prend le rôle de B, et oubliez A tout à fait.

Si vous ne le faites pas, ce n'est plus un test unitaire, mais un test système ou d'intégration. Celles-ci sont également très importantes, mais une toute autre marmite de poisson. Pour commencer, ils nécessitent généralement plus d'efforts pour être configurés et exécutés, il n'est pas possible d'exiger de les passer comme condition préalable à l'enregistrement, etc.

21
Kilian Foth

La réalisation de tests unitaires par rapport à une connexion à une base de données est parfaitement normale et une pratique courante. Il n'est tout simplement pas possible de créer une approche purist où tout dans votre système est injectable en dépendance.

La clé ici est de tester par rapport à une base de données temporaire ou de test uniquement, et d'avoir le processus de démarrage le plus léger possible pour créer cette base de données de test.

Pour les tests unitaires dans CakePHP, il y a des choses appelées fixtures. Les appareils sont des tables de base de données temporaires créées à la volée pour un test unitaire. Le luminaire a des méthodes pratiques pour les créer. Ils peuvent recréer un schéma à partir d'une base de données de production à l'intérieur de la base de données de test, ou vous pouvez définir le schéma à l'aide d'une notation simple.

La clé du succès est de ne pas implémenter la base de données d'entreprise, mais de se concentrer uniquement sur l'aspect du code que vous testez. Si vous disposez d'un test unitaire qui vérifie qu'un modèle de données lit uniquement les documents publiés, le schéma de table pour ce test ne doit contenir que les champs requis par ce code. Vous n'avez pas à réimplémenter une base de données de gestion de contenu entière juste pour tester ce code.

Quelques références supplémentaires.

http://en.wikipedia.org/wiki/Test_fixture

http://phpunit.de/manual/3.7/en/database.html

http://book.cakephp.org/2.0/en/development/testing.html#fixtures

12
Reactgular

Il y a, quelque part dans votre base de code, une ligne de code qui effectue l'action réelle de connexion à la base de données distante. Cette ligne de code est, 9 fois sur 10, un appel à une méthode "intégrée" fournie par les bibliothèques d'exécution spécifiques à votre langue et à votre environnement. En tant que tel, ce n'est pas "votre" code et vous n'avez donc pas besoin de le tester; aux fins d'un test unitaire, vous pouvez être sûr que cet appel de méthode fonctionnera correctement. Ce que vous pouvez, et devriez, encore tester dans votre suite de tests unitaires, ce sont des choses comme s’assurer que les paramètres qui seront utilisés pour cet appel sont ce que vous attendez d’eux, comme s’assurer que la chaîne de connexion est correcte, ou l’instruction SQL ou nom de la procédure stockée.

C'est l'un des objectifs de la restriction selon laquelle les tests unitaires ne doivent pas quitter leur "bac à sable" d'exécution et être dépendants de l'état externe. C'est en fait assez pratique; le but d'un test unitaire est de vérifier que le code vous avez écrit (ou est sur le point d'écrire, en TDD) se comporte comme vous le pensiez. Le code que vous n'avez pas écrit, comme la bibliothèque que vous utilisez pour effectuer vos opérations de base de données, ne devrait pas faire partie de la portée de tout test unitaire, pour la raison très simple que vous ne l'avez pas écrit.

Dans votre intégration suite de tests, ces restrictions sont assouplies. Maintenant, vous pouvez concevoir des tests qui touchent la base de données, pour vous assurer que le code que vous avez écrit joue bien avec le code que vous n'avez pas. Ces deux suites de tests doivent cependant rester séparées, car votre suite de tests unitaires est plus efficace plus elle s'exécute rapidement (vous pouvez donc vérifier rapidement que toutes les assertions faites par les développeurs à propos de leur code sont toujours valables), et presque par définition, un test d'intégration est plus lent de plusieurs ordres de grandeur en raison des dépendances supplémentaires sur les ressources externes. Laissez le build-bot gérer l'exécution de votre suite d'intégration complète toutes les quelques heures, exécutant les tests qui verrouillent les ressources externes, afin que les développeurs ne se marchent pas les uns les autres en exécutant ces mêmes tests localement. Et si la construction se casse, alors quoi? Il est beaucoup plus important de veiller à ce que le build-bot n'échoue jamais à une build, ce qui devrait probablement l'être.


Maintenant, la rigueur avec laquelle vous pouvez y adhérer dépend de votre stratégie exacte de connexion et d'interrogation de la base de données. Dans de nombreux cas, où vous devez utiliser l'infrastructure d'accès aux données "bare-bones", comme les objets SqlConnection et SqlStatement d'ADO.NET, une méthode entière développée par vous peut consister en des appels de méthode intégrés et d'autres codes qui dépendent de la présence d'un connexion à la base de données, et donc le mieux que vous puissiez faire dans cette situation est de simuler l'ensemble de la fonction et de faire confiance à vos suites de tests d'intégration. Cela dépend également de votre volonté de concevoir vos classes pour permettre le remplacement de lignes de code spécifiques à des fins de test (comme la suggestion de Tobi du modèle de méthode de modèle, qui est une bonne solution car elle permet des "simulations partielles" qui exercent certains méthodes d'une classe réelle tout en remplaçant les autres qui ont des effets secondaires).

Si votre modèle de persistance des données repose sur le code de votre couche de données (comme les déclencheurs, les processus stockés, etc.), il n'y a tout simplement pas d'autre moyen d'exercer le code que vous écrivez que de développer des tests qui vivent à l'intérieur de la couche de données ou traversent la frontière entre le runtime de votre application et le SGBD. Un puriste dirait que ce modèle, pour cette raison, doit être évité en faveur de quelque chose comme un ORM. Je ne pense pas que j'irais aussi loin; même à l'ère des requêtes intégrées au langage et d'autres opérations de persistance dépendant du domaine vérifiées par le compilateur, je vois l'intérêt de verrouiller la base de données uniquement aux opérations exposées via une procédure stockée, et bien sûr, ces procédures stockées doivent être vérifiées à l'aide de tests. Mais, ces tests ne sont pas des tests nit. Ce sont intégration tests.

Si vous rencontrez un problème avec cette distinction, elle est généralement basée sur une grande importance accordée à la "couverture de code" complète "couverture de test unitaire". Vous voulez vous assurer que chaque ligne de votre code est couverte par un test unitaire. Un noble objectif sur son visage, mais je dis hogwash; cette mentalité se prête à des anti-modèles qui s'étendent bien au-delà de ce cas spécifique, tels que l'écriture de tests sans assertion qui s'exécutent mais ne exercice votre code. Ces types de fin de parcours uniquement à des fins de numéros de couverture sont plus dangereux que d'assouplir votre couverture minimale. Si vous voulez vous assurer que chaque ligne de votre base de code est exécutée par un test automatisé, alors c'est facile; lors du calcul des mesures de couverture de code, incluez les tests d'intégration. Vous pourriez même aller plus loin et isoler ces tests "Itino" contestés ("Intégration dans le nom uniquement"), et entre votre suite de tests unitaires et cette sous-catégorie de tests d'intégration (qui devraient toujours fonctionner raisonnablement rapidement), vous devriez être sacrément presque proche d'une couverture complète.

4
KeithS

Les tests unitaires ne doivent jamais se connecter à une base de données. Par définition, ils doivent tester chacun une seule unité de code (une méthode) en totale isolation du reste de votre système. S'ils ne le font pas, ce n'est pas un test unitaire.

Mis à part la sémantique, il existe une multitude de raisons pour lesquelles cela est bénéfique:

  • Les tests exécutent des ordres de grandeur plus rapidement
  • La boucle de rétroaction devient instantanée (<1 s de rétroaction pour TDD, par exemple)
  • Les tests peuvent être exécutés en parallèle pour les systèmes de construction/déploiement
  • Les tests n'ont pas besoin d'une base de données pour être exécutés (rend la construction beaucoup plus facile, ou au moins plus rapide)

Les tests unitaires sont un moyen de vérifier votre travail. Ils doivent décrire tous les scénarios pour une méthode donnée, ce qui signifie généralement tous les différents chemins à travers une méthode. C'est votre spécification que vous construisez, semblable à la comptabilité à double entrée.

Ce que vous décrivez est un autre type de test automatisé: un test d'intégration. Bien qu'ils soient également très importants, vous en aurez idéalement beaucoup moins. Ils doivent vérifier qu'un groupe d'unités s'intègre correctement.

Alors, comment testez-vous les choses avec l'accès à la base de données? Tous vos codes d'accès aux données doivent être dans une couche spécifique, afin que votre code d'application puisse interagir avec des services modifiables au lieu de la base de données réelle. Il ne devrait pas se soucier de savoir si ces services sont soutenus par tout type de base de données SQL, des données de test en mémoire ou même des données de service Web distantes. Ce n'est pas leur préoccupation.

Idéalement (et c'est très subjectif), vous voulez que la majeure partie de votre code soit couverte par des tests unitaires. Cela vous donne l'assurance que chaque pièce fonctionne indépendamment. Une fois les pièces construites, vous devez les assembler. Exemple - lorsque je hache le mot de passe de l'utilisateur, je dois obtenir cette sortie exacte.

Disons que chaque composant est composé d'environ 5 classes - vous voudriez tester tous les points de défaillance en leur sein. Cela équivaut à beaucoup moins de tests juste pour s'assurer que tout est correctement câblé. Exemple - test, vous pouvez trouver l'utilisateur à partir de la base de données avec un nom d'utilisateur/mot de passe.

Enfin, vous voulez que certains tests d'acceptation garantissent réellement que vous atteignez les objectifs commerciaux. Il y en a encore moins; ils peuvent s'assurer que l'application fonctionne et fait ce pour quoi elle a été conçue. Exemple - compte tenu de ces données de test, je devrais pouvoir me connecter.

Considérez ces trois types de tests comme une pyramide. Vous avez besoin de nombreux tests unitaires pour tout prendre en charge, puis vous progressez à partir de là.

2
Adrian Schneider