web-dev-qa-db-fra.com

Quelle est la meilleure stratégie de test unitaire des applications pilotées par une base de données?

Je travaille avec beaucoup d'applications Web basées sur des bases de données de complexité variable. Généralement, il existe une couche ORM distincte de la logique métier et de la logique de présentation. Cela rend le test unitaire de la logique métier assez simple; les choses peuvent être implémentées dans des modules discrets et toutes les données nécessaires au test peuvent être falsifiées par le biais de moqueries 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é plusieurs 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 sur l'ORM et confirmez que les bonnes données sont renvoyées. L'inconvénient est que votre base de données de test doit suivre tout changement de schéma dans la base de données de l'application et risque de ne plus être synchronisée. Il s’appuie également sur des données artificielles et ne risque pas d’exposer des bugs dus à une entrée utilisateur stupide. Enfin, si la base de données de test est petite, elle ne révèlera pas les inefficacités telles qu'un index manquant. (OK, ce dernier point n'est pas vraiment ce pour quoi le test unitaire devrait être utilisé, mais ça ne fait pas 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 qu'il y a dans la base de données de production à un moment donné. vos tests devront peut-être être réécrits si les données changent avec le temps.

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

  • Utilisez un serveur de base de données fictif 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 a fonctionné le mieux pour vous?

321
friedo

En fait, j'ai utilisé votre première approche avec un certain succès, mais d'une manière légèrement différente qui, selon moi, résoudrait certains de vos problèmes:

  1. Conservez l'intégralité du schéma et des scripts permettant de le créer dans le contrôle de 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 les exemples de données dans des fichiers de données chargés par une partie du processus de construction. Lorsque vous découvrez des données générant des erreurs, ajoutez-les à vos exemples de données pour vérifier qu'elles 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 synchronisons notre base de données de tests (en la reconstruisant à chaque exécution de test). Bien que cela nécessite que le serveur de CI ait accès à sa propre instance de base de données dédiée et en soit le propriétaire, je dis que la construction de notre schéma de base de données 3 fois par jour a considérablement aidé à détecter les erreurs qui n'auraient probablement pas été détectées jusque juste 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’y serez pas obligé (eh bien, nous devrions peut-être, mais ce n’est pas grave si quelqu'un oublie).

  3. Pour mon groupe, la saisie de l'utilisateur se fait au niveau de l'application (pas de la base de données), elle est donc testée via des tests unitaires standard.

Chargement de la copie de la base de données de production:
C’est cette approche qui a été utilisée lors de mon dernier emploi. Ce fut une énorme douleur causant plusieurs problèmes:

  1. La copie serait périmée par rapport à 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.

Mocking serveur de base de données:
Nous le faisons également à mon poste actuel. Après chaque validation, nous exécutons des tests unitaires sur le code de l'application ayant injecté des accesseurs de base de données factices. Trois fois par jour, nous exécutons la compilation complète de la base de données décrite ci-dessus. Je recommande définitivement les deux approches.

146
Mark Roddy

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

  • Cela vous fait penser quelles données garder dans votre base de test et pourquoi. Transférer simplement votre base de données de production dans un système de test signifie "je ne sais pas ce que je fais ou pourquoi et si quelque chose se brise, ce n'est pas moi !!" ;)
  • Cela garantit que la base de données peut être recréée sans effort dans un nouvel emplacement (par exemple, lorsque nous devons répliquer un bogue de la production).
  • Cela aide énormément avec la qualité des fichiers DDL.

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

Les données sont chargées depuis SQL, une base de données modèle ou une sauvegarde/sauvegarde. Je préfère les dumps 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 performance sont un autre problème et différentes règles s'appliquent.

55
Aaron Digulla

Je pose cette question depuis longtemps, mais je pense qu’il n’ya pas de solution miracle à cela.

Ce que je fais actuellement est de se moquer des objets DAO et de garder en mémoire une bonne collection d'objets représentant des cas intéressants de données pouvant vivre dans 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 je constate que de nombreuses erreurs se produisent également sur cette couche. Je conserve également quelques tests unitaires exécutés 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 à cette fin. pense que les tests exécutés sur le serveur CI doivent être autonomes.

Une autre approche que je trouve très intéressante, mais qui ne vaut pas toujours la peine, étant donné que cela prend un peu de temps, est de créer le même schéma que celui que vous utilisez pour la production sur une base de données intégrée qui ne fait que s’exécuter dans les tests unitaires.

Même s'il ne fait aucun doute que cette approche améliore votre couverture, il y a quelques inconvénients, car vous devez être aussi proche que possible de la norme ANSI SQL pour que cela fonctionne à la fois avec votre SGBD actuel et avec le remplacement incorporé.

Peu importe ce que vous pensez être plus pertinent pour votre code, il existe quelques projets qui pourraient faciliter les choses, comme DbUnit .

14
kolrie

Même s’il existe des outils vous permettant de simuler votre base de données d’une manière ou d’une autre (par exemple, jOOQ ( MockConnection ), ce qui peut être vu dans - cette réponse - disclaimer, je travaille pour le fournisseur de jOOQ), je conseillerais de ne pas se moquer de bases de données plus volumineuses contenant des requêtes complexes.

Même si vous souhaitez simplement tester votre ORM par intégration, sachez qu’un ORM envoie une série très complexe de requêtes à votre base de données.

  • syntaxe
  • complexité
  • ordre (!)

Il est très difficile de simuler tout cela pour produire des données factices sensibles, sauf si vous construisez réellement 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 d’intégration-tests bien connue que vous pouvez facilement réinitialiser avec des données connues, sur laquelle vous pouvez exécuter vos tests d’intégration.

12
Lukas Eder

J'utilise le premier (exécuter le code sur une base de données de test). Le seul problème de fond que je vois que vous soulevez avec cette approche est la possibilité de désynchronisation des schémas, que je gère en conservant un numéro de version dans ma base de données et en effectuant toutes les modifications de schéma via un script qui les applique à chaque incrément de version.

J'apporte également toutes les modifications (y compris au schéma de base de données) par rapport à mon environnement de test, ce qui aboutit à l'inverse: une fois tous les tests terminés, appliquez les mises à jour du schéma à l'hôte de production. Je conserve également une paire séparée de bases de données de tests et d'applications 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 au (x) véritable (s) boîtier (s) de production.

5
Dave Sherohman

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 le nécessaire pour exécuter des tests pour les DAO se trouve dans le contrôle de source. Il comprend un schéma et des scripts pour créer la base de données (le menu fixe est très utile pour cela). Si la base de données intégrée peut être utilisée, je l’utilise par rapidité.

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 effectivement constantes) est créé par l'application à l'aide de fonctions/classes d'utilitaires.

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

  1. très proche de l'épreuve
  2. explicite (utiliser des fichiers SQL pour les données rend très problématique de savoir quelle donnée est utilisée par quel test)
  3. isoler les tests des modifications non liées.

Cela signifie fondamentalement que ces utilitaires permettent de spécifier de manière déclarative uniquement les éléments essentiels au test dans le test lui-même et d'omettre les éléments non pertinents.

Pour vous donner une idée de ce que cela signifie en pratique, considérons le test de certains DAO fonctionnant avec Comments à Posts écrit par Authors. Afin de tester les opérations CRUD pour ce type de DAO, certaines données doivent être créées dans la base de données. Le test ressemblerait à ceci:

@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 contenant des données de test:

  1. La maintenance du code est beaucoup plus facile (ajouter une colonne obligatoire, par exemple, dans une entité référencée dans de nombreux tests, comme Auteur, ne nécessite pas de modifier de nombreux fichiers/enregistrements, mais uniquement un changement de générateur 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é du test.

Rollback vs Commit

Je trouve plus pratique que les tests soient validés lorsqu'ils sont exécutés. Tout d’abord, certains effets (par exemple DEFERRED CONSTRAINTS) ne peut pas être cochée si la validation n’est jamais effectuée. 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 l'annulation.

Le test peut produire des données erronées, ce qui entraînera des échecs dans d’autres tests. Pour remédier à cela, j'essaie d'isoler les tests. Dans l'exemple ci-dessus, chaque test peut créer un nouveau Author et toutes les autres entités associées sont créées, de sorte que les collisions sont rares. Pour traiter les invariants restants qui peuvent être potentiellement cassés mais ne peuvent pas être exprimés sous la forme d'une contrainte de niveau de base de données, j'utilise certaines vérifications par programme pour détecter les conditions erronées pouvant être exécutées après chaque test (et exécutés dans un CI mais généralement désactivés localement pour des performances optimales). les raisons).

3
Roman Konoval

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

L’avantage est l’abstraction qui en découle, car les données JDBC (ensemble de résultats, nombre de mises à jour, avertissement, ...) sont identiques quel que soit le système: votre base de données prod, une base 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, rechargement des montages, ...). Chaque connexion de maquette est isolée et il n'est pas nécessaire de nettoyer. Dans chaque scénario de test, seuls les équipements requis minimes sont fournis pour simuler un échange JDBC, ce qui permet d'éviter la complexité de la gestion d'une base de données de test complète.

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

2
cchantep