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é:
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?
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:
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.
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).
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:
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.
J'exécute toujours des tests sur une base de données en mémoire (HSQLDB ou Derby) pour les raisons suivantes:
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.
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 .
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.
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.
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.
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
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 Comment
s à Post
s é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:
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).
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 .