Quelle est la meilleure stratégie pour tester les applications pilotées par une base de données?

346

Je travaille avec beaucoup d'applications Web qui sont pilotées par des bases de données de complexité variable sur le backend. En règle générale, il y a un couche ORM distincte de la logique métier et de la présentation. Cela rend les tests unitaires de la logique métier assez simples; les choses peuvent être implémentées dans des modules discrets et toutes les données nécessaires au test peuvent être truquées par le biais de la simulation d'objets.

Mais tester l'ORM et la base de données elle-même a toujours été semé d'embûches et de compromis.

Au fil des ans, j'ai essayé quelques stratégies, dont aucune ne m'a complètement satisfait.

  • Chargez une base de données de test avec des données connues. Exécutez des tests par rapport à l'ORM et confirmez que les bonnes données reviennent. L'inconvénient ici est que votre base de données de test doit suivre les modifications de schéma dans la base de données d'application et peut se désynchroniser. Il s'appuie également sur des données artificielles et ne peut pas exposer les bogues qui se produisent en raison d'une entrée utilisateur stupide. Enfin, si la base de données de test est petite, elle ne révélera pas des inefficacités comme un index manquant. (OK, ce dernier n'est pas vraiment pour quoi les tests unitaires devraient être utilisés, mais cela ne fait pas de mal.)

  • Chargez une copie de la base de données de production et testez-la. Le problème ici est que vous n'avez peut-être aucune idée de ce qui se trouve dans la base de données de production à un moment donné; vos tests devront peut-être être réécrits si les données changent au fil du temps.

Certaines personnes ont souligné que ces deux stratégies reposent sur des données spécifiques et qu'un test unitaire ne devrait tester que la fonctionnalité. À cette fin, j'ai vu suggéré:

  • Utilisez un serveur de base de données factice et vérifiez uniquement que l'ORM envoie les requêtes correctes en réponse à un appel de méthode donné.

Quelles stratégies avez-vous utilisées pour tester les applications basées sur une base de données, le cas échéant? Qu'est-ce qui vous a le mieux fonctionné?

friedo
la source
Je pense que vous devriez toujours avoir des index de base de données dans un environnement de test pour des cas comme des index uniques.
dtc
Je ne me soucie pas de cette question ici, mais si nous respectons les règles, cette question ne concerne pas stackoverflow, mais plutôt le site Web softwareengineering.stackexchange .
ITExpert

Réponses:

155

J'ai en fait utilisé votre première approche avec un certain succès, mais de manière légèrement différente, je pense que cela résoudrait certains de vos problèmes:

  1. Conservez l'intégralité du schéma et des scripts de création dans le contrôle de code source afin que tout le monde puisse créer le schéma de base de données actuel après une extraction. En outre, conservez des exemples de données dans des fichiers de données qui sont chargés par une partie du processus de génération. Lorsque vous découvrez des données qui provoquent des erreurs, ajoutez-les à vos exemples de données pour vérifier que les erreurs ne réapparaissent pas.

  2. Utilisez un serveur d'intégration continue pour créer le schéma de base de données, charger les exemples de données et exécuter des tests. C'est ainsi que nous gardons notre base de données de test synchronisée (en la reconstruisant à chaque test). Bien que cela nécessite que le serveur CI ait accès et possède sa propre instance de base de données dédiée, je dis que la création de notre schéma db 3 fois par jour a considérablement aidé à trouver des erreurs qui n'auraient probablement pas été trouvées avant la livraison (sinon plus tard) ). Je ne peux pas dire que je reconstruis le schéma avant chaque commit. Est-ce que quelqu'un? Avec cette approche, vous n'aurez pas à le faire (enfin peut-être que nous devrions, mais ce n'est pas un gros problème si quelqu'un oublie).

  3. Pour mon groupe, la saisie utilisateur se fait au niveau de l'application (et non db), ce qui est testé via des tests unitaires standard.

Chargement de la copie de la base de données de production:
c'était l'approche utilisée lors de mon dernier emploi. Ce fut une énorme cause de douleur pour quelques problèmes:

  1. La copie serait périmée de la version de production
  2. Des modifications seraient apportées au schéma de la copie et ne seraient pas propagées aux systèmes de production. À ce stade, nous aurions des schémas divergents. Pas drôle.

Serveur de base de données moqueur:
Nous faisons également cela dans mon travail actuel. Après chaque validation, nous exécutons des tests unitaires par rapport au code d'application qui ont injecté des accesseurs mock db. Ensuite, trois fois par jour, nous exécutons la version complète de la base de données décrite ci-dessus. Je recommande définitivement les deux approches.

Mark Roddy
la source
37
Le chargement d'une copie de la base de données de production a également des implications sur la sécurité et la confidentialité. Une fois qu'il est gros, en prendre une copie et le mettre dans votre environnement de développement peut être un gros problème.
WW.
Honnêtement, c'est une énorme douleur. Je suis nouveau dans les tests et j'ai également écrit un orm que je veux tester. J'ai déjà utilisé votre première méthode, mais lisez qu'elle ne fait pas l'unité de test. J'utilise des fonctionnalités spécifiques du moteur db et donc se moquer d'un DAO va être difficile. Je pense que j'utilise mal ma méthode actuelle car elle fonctionne et que d'autres l'utilisent. Tests automatisés rock btw. Merci.
frostymarvelous
2
Je gère deux grands projets différents, dans l'un d'eux, cette approche était parfaite, mais nous avons eu beaucoup de problèmes à essayer de l'implémenter dans l'autre projet. Je pense donc que cela dépend de la facilité avec laquelle le schéma pourrait être recréé à chaque fois pour exécuter les tests.Je travaille actuellement à trouver une nouvelle solution pour ce dernier problème.
Cross
2
Dans ce cas, il vaut vraiment la peine d'utiliser un outil de version de base de données comme Roundhouse - quelque chose qui peut exécuter des migrations. Cela peut être exécuté sur n'importe quelle instance de base de données et doit s'assurer que les schémas sont à jour. De plus, lors de l'écriture de scripts de migration, les données de test doivent également être écrites, en gardant les migrations et les données synchronisées.
jedd.ahyoung
mieux utiliser les correctifs et les
moquages ​​de
56

J'exécute toujours des tests sur une base de données en mémoire (HSQLDB ou Derby) pour ces raisons:

  • Cela vous fait penser aux données à conserver dans votre base de données de test et pourquoi. Le simple fait de transporter votre base de données de production dans un système de test se traduit par "Je n'ai aucune idée de ce que je fais ni pourquoi et si quelque chose se casse, ce n'est pas moi !!" ;)
  • Il s'assure que la base de données peut être recréée avec peu d'effort dans un nouvel endroit (par exemple lorsque nous devons répliquer un bogue de la production)
  • Cela aide énormément à la qualité des fichiers DDL.

La base de données en mémoire est chargée de nouvelles données une fois les tests démarrés et après la plupart des tests, j'appelle ROLLBACK pour la maintenir stable. TOUJOURS garder les données dans la base de données de test stable! Si les données changent tout le temps, vous ne pouvez pas tester.

Les données sont chargées à partir de SQL, d'une base de données de modèle ou d'une sauvegarde / sauvegarde. Je préfère les vidages s'ils sont dans un format lisible car je peux les mettre dans VCS. Si cela ne fonctionne pas, j'utilise un fichier CSV ou XML. Si je dois charger d'énormes quantités de données ... je ne le fais pas. Vous n'avez jamais à charger d'énormes quantités de données :) Pas pour les tests unitaires. Les tests de performances sont un autre problème et des règles différentes s'appliquent.

Aaron Digulla
la source
1
La vitesse est-elle la seule raison d'utiliser (spécifiquement) une base de données en mémoire?
rinogo
2
Je suppose qu'un autre avantage pourrait être sa nature "jetable" - pas besoin de nettoyer après vous-même; il suffit de tuer la base de données en mémoire. (Mais il existe d'autres moyens d'y parvenir, comme l'approche ROLLBACK que vous avez mentionnée)
rinogo
1
L'avantage est que chaque test peut choisir sa stratégie individuellement. Nous avons des tests qui font le travail dans les threads enfants, ce qui signifie que Spring valide toujours les données.
Aaron Digulla
@Aaron: nous suivons également cette stratégie. Je voudrais savoir quelle est votre stratégie pour affirmer que le modèle en mémoire a la même structure que le vrai db?
Guillaume
1
@Guillaume: Je crée toutes les bases de données à partir des mêmes fichiers SQL. H2 est idéal pour cela car il prend en charge la plupart des idiosyncrasies SQL des principales bases de données. Si cela ne fonctionne pas, j'utilise un filtre qui prend le SQL d'origine et le convertit en SQL pour la base de données en mémoire.
Aaron Digulla
14

Je pose cette question depuis longtemps, mais je pense qu'il n'y a pas de solution miracle pour cela.

Ce que je fais actuellement, c'est se moquer des objets DAO et garder en mémoire une bonne collection d'objets qui représentent des cas intéressants de données qui pourraient vivre sur la base de données.

Le principal problème que je vois avec cette approche est que vous ne couvrez que le code qui interagit avec votre couche DAO, mais que vous ne testez jamais le DAO lui-même, et d'après mon expérience, je constate que de nombreuses erreurs se produisent également sur cette couche. Je garde également quelques tests unitaires qui s'exécutent sur la base de données (dans le but d'utiliser TDD ou des tests rapides localement), mais ces tests ne sont jamais exécutés sur mon serveur d'intégration continue, car nous ne conservons pas de base de données à cet effet et je pense que les tests qui s'exécutent sur le serveur CI doivent être autonomes.

Une autre approche que je trouve très intéressante, mais qui ne vaut pas toujours car elle prend un peu de temps, consiste à créer le même schéma que vous utilisez pour la production sur une base de données intégrée qui s'exécute uniquement dans le cadre des tests unitaires.

Même s'il ne fait aucun doute que cette approche améliore votre couverture, il existe quelques inconvénients, car vous devez être aussi proche que possible de ANSI SQL pour le faire fonctionner à la fois avec votre SGBD actuel et le remplacement intégré.

Peu importe ce que vous pensez être plus pertinent pour votre code, il existe quelques projets qui peuvent le rendre plus facile, comme DbUnit .

kolrie
la source
13

Même s'il existe des outils qui vous permettent de se moquer de votre base de données d'une manière ou d'une autre (par exemple jOOQ d » MockConnection, qui peut être vu dans cette réponse - disclaimer, je travaille pour le fournisseur de jOOQ), je vous conseille pas se moquer de grandes bases de données avec complexes requêtes.

Même si vous voulez simplement tester l'intégration de votre ORM, sachez qu'un ORM émet une série très complexe de requêtes vers votre base de données, qui peuvent varier en

  • syntaxe
  • complexité
  • ordre (!)

Se moquer de tout cela pour produire des données factices sensibles est assez difficile, à moins que vous ne construisiez en fait une petite base de données à l'intérieur de votre maquette, qui interprète les instructions SQL transmises. Cela dit, utilisez une base de données de tests d'intégration bien connue que vous pouvez facilement réinitialiser avec des données bien connues, sur lesquelles vous pouvez exécuter vos tests d'intégration.

Lukas Eder
la source
5

J'utilise le premier (exécution du code sur une base de données de test). Le seul problème de fond que je vous vois soulever avec cette approche est la possibilité que les schémas se désynchronisent, que je traite en conservant un numéro de version dans ma base de données et en apportant toutes les modifications de schéma via un script qui applique les modifications pour chaque incrément de version.

J'apporte également toutes les modifications (y compris au schéma de la base de données) à mon environnement de test, donc cela finit par être l'inverse: une fois tous les tests réussis, appliquez les mises à jour du schéma à l'hôte de production. Je garde également une paire distincte de bases de données de test et d'application sur mon système de développement afin de pouvoir vérifier que la mise à niveau de la base de données fonctionne correctement avant de toucher la ou les boîtes de production réelles.

Dave Sherohman
la source
3

J'utilise la première approche mais un peu différente qui permet de résoudre les problèmes que vous avez mentionnés.

Tout ce qui est nécessaire pour exécuter des tests pour les DAO se trouve dans le contrôle de code source. Il comprend un schéma et des scripts pour créer la base de données (le docker est très bon pour cela). Si la base de données intégrée peut être utilisée - je l'utilise pour la vitesse.

La différence importante avec les autres approches décrites est que les données requises pour le test ne sont pas chargées à partir de scripts SQL ou de fichiers XML. Tout (sauf certaines données de dictionnaire qui sont effectivement constantes) est créé par l'application à l'aide de fonctions / classes utilitaires.

Le but principal est de rendre les données utilisées par le test

  1. très proche du test
  2. explicite (l'utilisation de fichiers SQL pour les données rend très difficile de voir quel élément de données est utilisé par quel test)
  3. isoler les tests des changements indépendants.

Cela signifie essentiellement que ces utilitaires permettent de spécifier de manière déclarative uniquement les choses essentielles pour le test dans le test lui-même et d'omettre les choses non pertinentes.

Pour donner une idée de ce que cela signifie dans la pratique, considérez le test de certains DAO qui fonctionnent avec les Comments à Posts écrits par Authors. Afin de tester les opérations CRUD pour un tel DAO, certaines données doivent être créées dans la base de données. Le test ressemblerait à:

@Test
public void savedCommentCanBeRead() {
    // Builder is needed to declaratively specify the entity with all attributes relevant
    // for this specific test
    // Missing attributes are generated with reasonable values
    // factory's responsibility is to create entity (and all entities required by it
    //  in our example Author) in the DB
    Post post = factory.create(PostBuilder.post());

    Comment comment = CommentBuilder.comment().forPost(post).build();

    sut.save(comment);

    Comment savedComment = sut.get(comment.getId());

    // this checks fields that are directly stored
    assertThat(saveComment, fieldwiseEqualTo(comment));
    // if there are some fields that are generated during save check them separately
    assertThat(saveComment.getGeneratedField(), equalTo(expectedValue));        
}

Cela présente plusieurs avantages par rapport aux scripts SQL ou aux fichiers XML avec des données de test:

  1. La maintenance du code est beaucoup plus facile (l'ajout d'une colonne obligatoire par exemple dans une entité référencée dans de nombreux tests, comme Author, ne nécessite pas de modifier beaucoup de fichiers / enregistrements mais seulement un changement de constructeur et / ou d'usine)
  2. Les données requises par un test spécifique sont décrites dans le test lui-même et non dans un autre fichier. Cette proximité est très importante pour la compréhensibilité des tests.

Rollback vs Commit

Je trouve plus pratique que les tests soient validés lorsqu'ils sont exécutés. Premièrement, certains effets (par exempleDEFERRED CONSTRAINTS ) ne peuvent pas être vérifiés si la validation ne se produit jamais. Deuxièmement, lorsqu'un test échoue, les données peuvent être examinées dans la base de données car elles ne sont pas annulées par la restauration.

Cela a pour inconvénient que le test peut produire des données cassées et cela entraînera des échecs dans d'autres tests. Pour y faire face, j'essaie d'isoler les tests. Dans l'exemple ci-dessus, chaque test peut en créer de nouvelles Authoret toutes les autres entités sont créées en relation avec celui-ci, les collisions sont donc rares. Pour gérer les invariants restants qui peuvent être potentiellement cassés mais ne peuvent pas être exprimés en tant que contrainte de niveau DB, j'utilise des vérifications programmatiques pour les conditions erronées qui peuvent être exécutées après chaque test (et elles sont exécutées en CI mais généralement désactivées localement pour des performances) les raisons).

Roman Konoval
la source
Si vous amorcez la base de données à l'aide d'entités et de l'orm au lieu de scripts sql, cela a également l'avantage que le compilateur vous forcera à corriger le code d'origine si vous apportez des modifications à votre modèle. Uniquement pertinent si vous utilisez un langage tapé statique bien sûr.
daramasala
Donc, pour plus de précision: utilisez-vous les fonctions / classes utilitaires tout au long de votre application, ou tout simplement pour vos tests?
Ella
@Ella ces fonctions utilitaires ne sont généralement pas nécessaires en dehors du code de test. Pensez par exemple à PostBuilder.post(). Il génère des valeurs pour tous les attributs obligatoires de la publication. Ce n'est pas nécessaire dans le code de production.
Roman Konoval
2

Pour un projet basé sur JDBC (directement ou indirectement, par exemple JPA, EJB, ...), vous pouvez créer une maquette non pas la base de données entière (dans ce cas, il serait préférable d'utiliser une base de données de test sur un vrai SGBDR), mais uniquement une maquette au niveau JDBC .

L'avantage est l'abstraction qui vient de cette façon, car les données JDBC (jeu de résultats, nombre de mises à jour, avertissement, ...) sont les mêmes quel que soit le backend: votre base de données de prod, une base de données de test ou juste quelques données de maquette fournies pour chaque test Cas.

Avec la connexion JDBC simulée pour chaque cas, il n'est pas nécessaire de gérer la base de données de test (nettoyage, un seul test à la fois, recharger les appareils, ...). Chaque connexion maquette est isolée et il n'est pas nécessaire de nettoyer. Seuls les équipements requis minimaux sont fournis dans chaque scénario de test pour simuler l'échange JDBC, ce qui permet d'éviter la complexité de la gestion d'une base de données de test entière.

Acolyte est mon framework qui comprend un pilote JDBC et un utilitaire pour ce type de maquette: http://acolyte.eu.org .

cchantep
la source