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

StackOverflow https://stackoverflow.com/questions/145131

Question

Je travaille avec de nombreuses applications Web gérées par des bases de données de complexité variable sur le backend. Il existe généralement une couche ORM distincte de la logique de gestion et 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 la moquerie 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 du contenu de 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 déjà 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 pilotées par une base de données, le cas échéant? Qu'est-ce qui a fonctionné le mieux pour vous?

Était-ce utile?

La solution

J'ai en fait 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 se reproduisent 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 jusqu'à 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 (et non 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:
Cette approche 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 amusant.

Mocking Database Server:
Nous le faisons également dans mon travail 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.

Autres conseils

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 aux données à conserver 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.

Le DB en mémoire est chargé 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 maintenez les données de la base de 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.

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 qui représentent 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 sur cette couche. bien. Je conserve également quelques tests unitaires exécutés sur la base de données (pour pouvoir 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 et 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 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 existe quelques inconvénients, car vous devez être aussi proche que possible du code SQL ANSI pour le faire fonctionner à la fois avec votre SGBD actuel et avec le remplacement incorporé.

Quoi que vous pensiez être plus pertinent pour votre code, il existe quelques projets qui pourraient vous faciliter la tâche, tels que DbUnit .

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 ' s MockConnection a>, visible dans cette réponse - disclaimer, je travaille pour le fournisseur de jOOQ), je conseillerais pas de se moquer de bases de données plus volumineuses avec 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, ce qui peut varier dans

  • syntaxe
  • complexité
  • commande (!)

Il est assez difficile de se moquer de 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. Ceci dit, utilisez une base de données d’intégration-tests bien connue, que vous pouvez facilement réinitialiser avec des données connues et sur laquelle vous pouvez exécuter vos tests d’intégration.

J'utilise le premier (exécuter le code sur une base de données de test). La seule question de fond que je vous vois soulever 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'effectue également toutes les modifications (y compris sur le schéma de base de données) dans 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 test 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.

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 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 souci de 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 du test
  2. explicite (utiliser des fichiers SQL pour les données rend très difficile de savoir quelle donnée est utilisée par quel test)
  3. isolez 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 des Commentaires à Post écrits par Auteurs . 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 à:

@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 simple (ajouter une colonne obligatoire, par exemple, dans une entité référencée dans de nombreux tests, comme Author, 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.

Annulation / validation

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 peuvent pas être vérifiés si la validation n'a jamais lieu. 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.

L’un des inconvénients de ce test est que 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 Auteur et toutes les autres entités créées sont associé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ées dans un CI mais généralement désactivées localement pour des performances raisons).

Pour un projet basé sur JDBC (directement ou indirectement, par exemple JPA, EJB, ...), vous ne pouvez pas créer de maquette de la base de données complète (dans ce cas, il serait préférable d’utiliser une base de test sur un SGBDR réel), mais uniquement des maquettes. au niveau 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 les mêmes quel que soit le backend: votre base de données prod, une base de test ou simplement quelques données de maquette fournies pour chaque cas de test.

La connexion JDBC étant simulée pour chaque cas, il n’est pas nécessaire de gérer la base de données test (nettoyage, un seul test à la fois, rechargement des appareils, ...). Chaque connexion de maquette est isolée et il n'est pas nécessaire de nettoyer. Pour simuler un échange JDBC, seul le nombre minimal de luminaires requis est fourni dans chaque scénario de test, 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 .

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top