web-dev-qa-db-fra.com

Comment écrire des tests unitaires pour les appels de base de données

Je suis au début d'un nouveau projet et (halètement!) Pour la première fois, j'essaie d'inclure des tests unitaires dans un de mes projets.

J'ai du mal à concevoir certains des tests unitaires eux-mêmes. J'ai quelques méthodes qui ont été assez faciles à tester (passez deux valeurs et vérifiez une sortie attendue). J'ai d'autres parties du code qui font des choses plus complexes comme exécuter des requêtes sur la base de données et je ne sais pas comment les tester.

public DataTable ExecuteQuery(SqlConnection ActiveConnection, string Query, SqlParameterCollection Parameters)
{
    DataTable resultSet = new DataTable();
    SqlCommand queryCommand = new SqlCommand();
    try
    {
        queryCommand.Connection = ActiveConnection;
        queryCommand.CommandText = Query;

        if (Parameters != null)
        {
            foreach (SqlParameter param in Parameters)
            {
                 queryCommand.Parameters.Add(param);
            }
        }

        SqlDataAdapter queryDA = new SqlDataAdapter(queryCommand);
        queryDA.Fill(resultSet);
    }
    catch (Exception ex)
    {
        //TODO: Improve error handling
        Console.WriteLine(ex.Message);
    }

    return resultSet;
}

Cette méthode prend essentiellement tous les bits et morceaux nécessaires pour extraire certaines données de la base de données et renvoie les données dans un objet DataTable.

La première question est probablement la plus complexe: que dois-je même tester dans une situation comme celle-ci?

Une fois que cela est réglé, vient la question de savoir s'il faut ou non simuler les composants de la base de données ou essayer de tester par rapport à la base de données réelle.

61
kdmurray

Que testez-vous?

Il y a trois possibilités, du haut de ma tête:

A. Vous testez la classe DAO (objet d'accès aux données), en vous assurant qu'elle rassemble correctement les valeurs/paramètres transmis à la base de données, et que les résultats correctement rassemblés/transformés/empaquetés sont récupérés dans la base de données.

Dans ce cas, vous n'avez pas du tout besoin de vous connecter à la base de données; vous avez juste besoin d'un test unitaire qui remplace la base de données (ou la couche intermédiaire, par exemple, JDBC, (N) Hibernate, iBatis) par une maquette.

B. Vous testez l'exactitude syntaxique du SQL (généré).

Dans ce cas, étant donné que les dialectes SQL diffèrent, vous souhaitez exécuter le SQL (éventuellement généré) sur la version correcte de votre SGBDR, plutôt que de tenter de se moquer de toutes les bizarreries de votre SGBDR (et de sorte que toutes les mises à niveau de SGBDR qui changent de fonctionnalité soient capturées par vos tests).

C. Vous testez l'exactitude sémantique de votre SQL, c'est-à-dire que pour un ensemble de données de base donné, vos opérations (accès/sélections et mutations/insertions) et mises à jour) produisent le nouvel ensemble de données attendu.

Pour cela, vous voulez utiliser quelque chose comme dbunit (qui vous permet de configurer une ligne de base et de comparer un jeu de résultats à un jeu de résultats attendu), ou éventuellement de faire vos tests entièrement dans la base de données, en utilisant la technique que je décris ici: - Meilleure façon de tester les requêtes SQL .

46
tpdi

C'est pourquoi les tests unitaires (à mon humble avis) peuvent parfois créer un faux sentiment de sécurité de la part des développeurs. D'après mon expérience avec les applications qui communiquent avec une base de données, les erreurs sont généralement le résultat de données dans un état inattendu (valeurs inhabituelles ou manquantes, etc.). Si vous simulez régulièrement l'accès aux données dans vos tests unitaires, vous penserez que votre code fonctionne très bien alors qu'il est en fait encore vulnérable à ce type d'erreur.

Je pense que votre meilleure approche est d'avoir une base de données de test à portée de main, remplie de tas de données merdiques, et d'exécuter vos tests de composants de base de données contre cela. Tout en vous rappelant que vos utilisateurs seront bien meilleurs que vous pour visser vos données.

28
MusiGenesis

L'intérêt d'un test unitaire est de tester une unité (duh) isolément. L'intérêt d'un appel à une base de données est de intégrer avec une autre unité (la base de données). Ergo: cela n'a aucun sens de tester les appels de base de données.

Vous devez cependant tester les appels de base de données d'intégration (et vous pouvez utiliser les mêmes outils que vous utilisez pour les tests unitaires si vous le souhaitez).

10
Jörg W Mittag

Pour l'amour de Dieu, ne testez pas une base de données en direct et déjà peuplée. Mais tu le savais.

En général, vous avez déjà une idée du type de données que chaque requête va récupérer, que vous authentifiiez des utilisateurs, que vous recherchiez des entrées de répertoire/organigramme, ou autre chose. Vous savez quels champs vous intéressent et vous savez quelles contraintes y sont associées (par exemple, UNIQUE, NOT NULL, etc). Vous testez à l'unité votre code qui interagit avec la base de données, pas avec la base de données elle-même, alors réfléchissez à la façon de tester ces fonctions. S'il est possible qu'un champ soit NULL, vous devriez avoir un test qui s'assure que votre code gère correctement les valeurs NULL. Si l'un de vos champs est une chaîne (CHAR, VARCHAR, TEXT, & c), testez pour vous assurer que vous gérez correctement les caractères d'échappement.

Supposons que les utilisateurs tentent de mettre quoi que ce soit * dans la base de données et génèrent des cas de test en conséquence. Vous voudrez utiliser des objets fictifs pour cela.

* Y compris les entrées indésirables, malveillantes ou invalides.

6

À strictement parler, un test qui écrit/lit à partir d'une base de données ou d'un système de fichiers n'est pas un test unitaire. (Bien qu'il puisse s'agir d'un test d'intégration et qu'il peut être écrit à l'aide de NUnit ou JUnit). Les tests unitaires sont censés tester les opérations d'une seule classe, isolant ses dépendances. Ainsi, lorsque vous écrivez un test unitaire pour les couches d'interface et de logique métier, vous ne devriez pas du tout avoir besoin d'une base de données.

OK, mais comment testez-vous à l'unité la couche d'accès à la base de données? J'aime les conseils de ce livre: xUnit Test Patterns (le lien pointe vers le chapitre "Testing w/DB" du livre. Les clés sont:

  • utiliser des tests aller-retour
  • n'écrivez pas trop de tests dans votre appareil de test d'accès aux données, car ils s'exécuteront beaucoup plus lentement que vos "vrais" tests unitaires
  • si vous pouvez éviter de tester avec une vraie base de données, testez sans base de données
4
azheglov

Vous pouvez tout tester unitaire sauf: queryDA.Fill(resultSet);

Dès que vous exécutez queryDA.Fill(resultSet), vous devez soit simuler/simuler la base de données, soit effectuer des tests d'intégration.

Pour ma part, je ne vois pas les tests d'intégration comme étant mauvais, c'est juste qu'ils attraperont un type de bogue différent, ont des cotes différentes de faux négatifs et de faux positifs, ne sont pas susceptibles d'être effectués très souvent parce qu'ils le sont lent.

Si je testais ce code à l'unité, je validerais que les paramètres sont correctement construits, le générateur de commandes crée-t-il le bon nombre de paramètres? Ont-ils tous une valeur? Les valeurs nulles, les chaînes vides et DbNull sont-elles gérées correctement?

En réalité, remplir l'ensemble de données consiste à tester votre base de données, qui est un composant instable hors de la portée de votre DAL.

4
MatthewMartin

Pour les tests unitaires, je moque généralement ou fausse la base de données. Utilisez ensuite votre implémentation fausse ou fausse via l'injection de dépendances pour tester votre méthode. Vous auriez également probablement des tests d'intégration qui testeront les contraintes, les relations de clés étrangères, etc. dans votre base de données.

Quant à ce que vous testeriez, vous vous assureriez que la méthode utilise la connexion à partir des paramètres, que la chaîne de requête est affectée à la commande et que votre jeu de résultats renvoyé est le même que celui que vous fournissez via une attente sur la méthode Fill. Remarque - il est probablement plus facile de tester une méthode Get qui renvoie une valeur qu'une méthode Fill qui modifie un paramètre.

2
tvanfosson

Pour faire cela correctement, vous devez utiliser une injection de dépendance (DI), et pour .NET, il y en a plusieurs. J'utilise actuellement le Unity Framework mais il y en a d'autres qui sont plus faciles.

Voici un lien de ce site sur ce sujet, mais il y en a d'autres: Injection de dépendances dans .NET avec des exemples?

Cela vous permettrait de simuler plus facilement d'autres parties de votre application, simplement en ayant une classe simulée implémenter l'interface, afin que vous puissiez contrôler la façon dont elle répondra. Mais, cela signifie également la conception d'une interface.

Puisque vous avez posé des questions sur les meilleures pratiques, ce serait l'une d'entre elles, l'OMI.

Ensuite, ne pas aller à la base de données sauf si vous en avez besoin, comme suggéré est un autre.

Si vous devez tester certains comportements, tels que les relations de clés étrangères avec la suppression en cascade, vous pouvez écrire des tests de base de données pour cela, mais généralement ne pas aller dans une vraie base de données est préférable, d'autant plus que plus d'une personne peut exécuter un test unitaire à un moment et s'ils vont sur la même base de données, les tests peuvent échouer car les données attendues peuvent changer.

Edit: Par test unitaire de base de données, je veux dire cela, car il est conçu pour utiliser simplement t-sql pour effectuer une configuration, un test et une suppression. http://msdn.Microsoft.com/en-us/library/aa833233%28VS.80%29.aspx

1
James Black

Sur un projet basé sur JDBC, la connexion JDBC peut être simulée, afin que les tests puissent être exécutés sans RDBMS en direct, avec chaque cas de test isolé (pas de conflit de données).

Il permet de vérifier que le code de persistance transmet les requêtes/paramètres appropriés (par exemple https://github.com/playframework/playframework/blob/master/framework/src/anorm/src/test/scala/anorm/ParameterSpec. scala ) et gérer les résultats JDBC (analyse/mappage) comme prévu ("prend tous les bits et morceaux nécessaires pour extraire certaines données de la base de données et renvoie les données dans un objet DataTable").

Un framework comme jOOQ ou mon framework Acolyte peut être utilisé pour: https://github.com/cchantep/acolyte .

0
cchantep