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


37

Il existe des réponses à la question sur la manière 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 à une base de données " .

En bref, supposons que vous ayez une classe A qui doit se connecter à une base de données. Au lieu de laisser A réellement se connecter, vous fournissez à A une interface que A peut utiliser pour se connecter. Pour tester, vous implémentez cette interface avec quelques éléments - sans vous connecter bien sûr. Si la classe B instancie A, elle doit passer une connexion "réelle" à la base 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é en classe C et ainsi de suite.

Donc, à 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, je dois appeler sqlDB.connect()ou quelque chose de similaire. Comment puis-je tester cette classe?

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


Je veux faire des tests unitaires. Tout autre type de test n'est pas lié à ma question. Je sais que je ne testerai qu'une classe avec elle (je suis tellement d'accord avec toi Kilian). Maintenant, certaines classes doivent se connecter à une base de données. Si je veux tester cette classe et demander "Comment puis-je faire cela", beaucoup disent: "Utilisez l'injection de dépendance!" Mais cela ne fait que déplacer le problème vers 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 à "Utilisez 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 à l’essai maintenant et effectivement tester la maquette (qui se rapproche de l’idée d’utiliser des méthodes modèles, voir ci-dessous)?


Est-ce la connexion à la base de données que vous testez? La création d'une base de données temporaire en mémoire (telle que derby ) serait-elle acceptable?

@MichaelT Je dois encore remplacer le DB en mémoire temporaire par une base de données réelle. Où? Quand? Comment est-il testé en unité? Ou est-ce correct de ne pas tester ce code à l’unité?
TobiMcNamobi

3
Il n'y a rien à "tester un peu" sur la base de données. Il est maintenu par d'autres personnes, et s'il y avait un bogue dans celui-ci, vous devriez les laisser le réparer plutôt que de le faire vous-même. La seule chose qui devrait différer entre l'utilisation réelle de votre classe et l'utilisation lors des tests devrait être les paramètres de la connexion à la base de données. Il est peu probable que le code de lecture du fichier de propriétés, ou le mécanisme d'injection de Spring ou tout ce que vous utilisez pour tisser votre application ensemble soit brisé (et si c'était le cas, vous ne pourriez pas le réparer vous-même, voir ci-dessus) - je le considère donc comme acceptable. ne pas tester cette fonctionnalité de plomberie.
Kilian Foth

2
@KilianFoth qui est purement lié à l'environnement de travail et aux rôles des employés. Cela n'a rien à voir avec la question. Que se passe-t-il s'il n'y a pas un seul responsable de la base de données?
Reactgular

Certains frameworks moqueurs vous permettent d’injecter des objets fictifs dans pratiquement tout, même dans des membres privés et statiques. Cela facilite beaucoup les tests avec des choses comme les connexions mock db. Mockito + Powermock est ce qui fonctionne pour moi ces jours-ci (ils sont en Java, je ne sais pas dans quoi vous travaillez).
FrustratedWithFormsDesigner

Réponses:


21

L'intérêt d'un test unitaire est de tester une classe (en fait, il convient généralement de tester une méthode ).

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

Cependant, si vous testez la classe B, qui est un client de A, alors vous vous moquez de l' Aobjet entier avec autre chose, probablement quelque chose qui fait son travail de manière primitive et préprogrammée - sans utiliser d' Aobjet réel et certainement sans utiliser de données base (à moins que Al’ensemble de la connexion à la base de données ne soit renvoyée à 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 Bet qui vous fait oublier A.

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 représentent une tout autre variété de poissons. Pour commencer, ils demandent généralement plus d'efforts à configurer et à exécuter, il n'est pas pratique d'exiger de les transmettre comme condition préalable à l'enregistrement, etc.


11

Effectuer des tests unitaires avec une connexion à une base de données est parfaitement normal et courant. Il est tout simplement impossible de créer une puristapproche où tout dans votre système est une dépendance injectable.

La clé ici est de tester une base de données temporaire ou uniquement à tester, et d’avoir le processus de démarrage le plus léger possible pour créer cette base 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 projecteur a des méthodes pratiques pour les créer. Ils peuvent recréer un schéma à partir d'une base de production dans la base de tests, ou vous pouvez définir le schéma à l'aide d'une notation simple.

La clé du succès consiste à ne pas implémenter la base de données métier, mais à 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 ne lit que les documents publiés, le schéma de table de ce test ne doit contenir que les champs requis par ce code. Il n'est pas nécessaire de réimplémenter toute une base de données de gestion de contenu simplement 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/fr/development/testing.html#fixtures


28
Je ne suis pas d'accord. Un test nécessitant une connexion à une base de données n'est pas un test unitaire, car de par sa nature, le test aura des effets secondaires. Cela ne signifie pas que vous ne pouvez pas écrire de test automatisé, mais un tel test est par définition un test d'intégration, qui exerce sur votre système des zones au-delà de votre base de code.
KeithS

5
Appelez-moi un puriste, mais je tiens au principe qu'un test unitaire ne doit effectuer aucune action laissant le "bac à sable" de l'environnement d'exécution de test. Ils ne doivent pas toucher aux bases de données, aux systèmes de fichiers, aux sockets réseau, etc. Ceci est dû à plusieurs raisons, dont la moindre n'est pas la dépendance du test à un état externe. Un autre est la performance; votre suite de tests unitaires doit s'exécuter rapidement et l'interface avec ces données externes stocke les tests lents par ordre de grandeur. Dans mon propre développement, je me sers de simulacres partiels pour tester des objets comme mes référentiels, et je suis à l'aise pour définir un "bord" dans mon bac à sable.
KeithS

2
@ gbjbaanb - Cela semble bien au début, mais d'après mon expérience, c'est très dangereux. Même dans les suites de tests et les infrastructures les mieux architecturées, le code permettant d'annuler cette transaction peut ne pas s'exécuter. Si le lanceur de tests se bloque ou est abandonné dans un test, ou si le test jette un SOE ou OOME, le meilleur des cas est que vous ayez une connexion en attente et une transaction dans la base de données qui verrouillera les tables que vous avez touchées jusqu'à ce que la connexion soit interrompue. Les façons dont vous éviter ces problèmes causant, comme l' utilisation SQLite comme un test DB, ont leurs propres inconvénients, par exemple le fait que vous n'êtes pas vraiment l' exercice du réel DB.
KeithS

5
@ KeithS Je pense que nous débattons de la sémantique. Il ne s'agit pas de définir un test unitaire ou un test d'intégration. J'utilise des fixtures pour tester le code en fonction d'une connexion à une base de données. Si c'est un test d'intégration, ça me convient. J'ai besoin de savoir que le test réussit. Je me fous des dépendances, des performances ou des risques. Je ne saurai pas si ce code fonctionne si le test ne réussit pas. Pour la majorité des tests, il n'y a pas de dépendances, mais pour celles où il y en a, ces dépendances ne peuvent pas être découplées. Il est facile de dire qu'ils devraient l'être, mais ils ne peuvent tout simplement pas l'être.
Reactgular

4
Je pense que nous le sommes aussi. J'utilise également un "framework de tests unitaires" (NUnit) pour mes tests d'intégration, mais je m'assure toutefois de séparer ces deux catégories de tests (souvent dans des bibliothèques distinctes). Ce que j’essayais de dire, c’est que votre suite de tests unitaires, celle que vous exécutez plusieurs fois par jour avant chaque enregistrement lorsque vous suivez la méthode itérative rouge-vert-refactor, doit être complètement isolable, de sorte que vous puissiez exécuter ces tests plusieurs fois par jour sans marcher sur les pieds de vos collègues.
KeithS

4

Il existe, 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 toujours tester dans votre suite de tests unitaires, par exemple, vous assurer que les paramètres qui seront utilisés pour cet appel correspondent à ce que vous attendez, comme s’assurer que la chaîne de connexion est correcte, ou l’instruction SQL ou nom de 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 dépendent de l’état externe. C'est en fait assez pratique; le but d'un test unitaire est de vérifier que le code que vous avez écrit (ou êtes sur le point d'écrire, en TDD) se comporte comme vous le pensiez. Le code que vous n'avez pas écrit, tel que 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 d'un test unitaire, pour la simple raison que vous ne l'avez pas écrit.

Dans votre suite de tests d' intégration , ces restrictions sont assouplies. Maintenant vous pouvezDes tests de conception qui touchent la base de données, pour vous assurer que le code que vous avez écrit joue bien avec du code que vous n'avez pas. Cependant, ces deux suites de tests doivent rester séparées, car votre suite de tests unitaires est d'autant plus efficace qu'elle s'exécute rapidement (vous pouvez ainsi vérifier rapidement que toutes les affirmations des développeurs concernant leur code sont toujours valables) et, presque par définition, un test d'intégration. est plus lent par ordres de grandeur en raison des dépendances supplémentaires sur les ressources externes. Laissez-le-robot gérer l'exécution de votre suite d'intégration complète toutes les quelques heures, en exécutant les tests qui bloquent les ressources externes, afin que les développeurs ne se marchent pas l'un sur l'autre en exécutant ces mêmes tests localement. Et si la construction casse, et alors? Il est beaucoup plus important de s’assurer que le build-bot n’échoue jamais une construction comme il se doit.


Maintenant, le degré de stricte conformité avec cela dépend de votre stratégie exacte pour vous connecter à la base de données et l'interroger. Dans de nombreux cas où vous devez utiliser la structure d'accès aux données "à l'état brut", telle que les objets SqlConnection et SqlStatement d'ADO.NET, une méthode complète que vous avez développée peut être constituée d'appels de méthode intégrés et d'un autre code dépendant de la présence d'un objet. connexion de base de données, et le mieux que vous puissiez faire dans cette situation est de simuler toute la fonction et de faire confiance à vos suites de tests d’intégration. Cela dépend également de votre volonté de concevoir vos classes afin de permettre le remplacement de lignes de code spécifiques à des fins de test (comme la suggestion de Tobi concernant le modèle de méthode, qui est un bon modèle car il permet des "simulations partielles".

Si votre modèle de persistance des données repose sur du code de votre couche de données (tels que des déclencheurs, des processus stockés, etc.), il n’existe tout simplement pas d’autre moyen d’exercer du code que vous écrivez vous-même que de développer des tests qui vivent dans la couche de données ou qui traversent la couche de données. limite 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 dans la langue et des autres opérations de persistance dépendantes du domaine et vérifiées par le compilateur, je vois l'intérêt de verrouiller la base de données sur les seules opérations exposées via une procédure stockée. Bien entendu, ces procédures stockées doivent être vérifiées à l'aide de la méthode automatisée. tests. Mais, ces tests ne sont pas des tests unitaires . Ils sont l' 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, à savoir "couverture de test unitaire". Vous voulez vous assurer que chaque ligne de votre code est couverte par un test unitaire. Un objectif noble, mais je dis foutaise; cette mentalité se prête bien à des anti-schémas qui vont bien au-delà de ce cas particulier, tels que la rédaction de tests sans assertion qui s'exécutent sans exercervotre code. Ces types de fin d’année uniquement à des fins de couverture sont plus dommageables 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 métriques de couverture de code, incluez les tests d'intégration. Vous pouvez même aller un peu 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 devrait néanmoins fonctionner assez rapidement), vous devriez proche de la couverture complète.


2

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

Hormis la sémantique, il y a une myriade 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 (rétroaction <1 s pour TDD, à titre d'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 s'exécuter (rend la construction beaucoup plus facile, ou du 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 chemins différents d'une méthode. C’est votre spécification à laquelle vous vous adressez, semblable à la comptabilité en partie double.

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, idéalement, vous en aurez beaucoup moins. Ils doivent vérifier qu'un groupe d'unités s'intègre correctement les unes aux autres.

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 se trouver dans une couche spécifique, de sorte que le code de votre application puisse interagir avec des services modulables au lieu de la base de données réelle. Peu importe que ces services soient sauvegardés par un 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 souci.

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

Supposons que chaque composant se compose d'environ 5 classes. Vous souhaitez tester tous les points d'échec qui s'y trouvent. Cela équivaut à moins de tests pour s'assurer que tout est câblé correctement. Exemple - test vous pouvez trouver l'utilisateur dans la base de données à l'aide d'un nom d'utilisateur / mot de passe.

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

Pensez à ces trois types de tests comme une pyramide. Vous avez besoin de nombreux tests unitaires pour tout supporter, puis vous progressez à partir de là.


1

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

Vous encapsulez les appels à une base de données dans des protectedméthodes. Pour tester cette classe, vous testez en réalité un objet fictif qui hérite de la classe de connexion réelle à la base de données et remplace les méthodes protégées.

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


1
Au cas où vous vous demanderiez pourquoi je répondais à ma propre question: oui, cela pourrait être une réponse, mais je ne suis pas du tout sûr de savoir si c'est la bonne.
TobiMcNamobi

-1

Tester avec des données externes est un test d'intégration. Le test unitaire signifie que vous testez uniquement l'unité. Cela se fait principalement avec votre logique métier. Pour que votre unité de code soit testable, vous devez suivre certaines directives, tout comme vous devez rendre votre unité indépendante des autres parties de votre code. Lors du test unitaire, si vous avez besoin de données, vous devez alors injecter ces données avec une injection de dépendance. Il existe un cadre moqueur et moqueur.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.