Procédure de test unitaire d'un objet avec des requêtes de base de données

153

J'ai entendu dire que les tests unitaires sont «totalement géniaux», «vraiment cool» et «toutes sortes de bonnes choses», mais 70% ou plus de mes fichiers impliquent un accès à la base de données (certains en lecture et d'autres en écriture) et je ne sais pas comment pour écrire un test unitaire pour ces fichiers.

J'utilise PHP et Python mais je pense que c'est une question qui s'applique à la plupart / tous les langages qui utilisent l'accès à la base de données.

Teifion
la source

Réponses:

82

Je suggérerais de se moquer de vos appels à la base de données. Les simulacres sont essentiellement des objets qui ressemblent à l'objet sur lequel vous essayez d'appeler une méthode, dans le sens où ils ont les mêmes propriétés, méthodes, etc. disponibles pour l'appelant. Mais au lieu d'effectuer toute action pour laquelle ils sont programmés lorsqu'une méthode particulière est appelée, elle l'ignore complètement et renvoie simplement un résultat. Ce résultat est généralement défini par vous à l'avance.

Afin de configurer vos objets pour la moquerie, vous devez probablement utiliser une sorte d'inversion du modèle d'injection de contrôle / dépendance, comme dans le pseudo-code suivant:

class Bar
{
    private FooDataProvider _dataProvider;

    public instantiate(FooDataProvider dataProvider) {
        _dataProvider = dataProvider;
    }

    public getAllFoos() {
        // instead of calling Foo.GetAll() here, we are introducing an extra layer of abstraction
        return _dataProvider.GetAllFoos();
    }
}

class FooDataProvider
{
    public Foo[] GetAllFoos() {
        return Foo.GetAll();
    }
}

Maintenant, dans votre test unitaire, vous créez une maquette de FooDataProvider, qui vous permet d'appeler la méthode GetAllFoos sans avoir à accéder à la base de données.

class BarTests
{
    public TestGetAllFoos() {
        // here we set up our mock FooDataProvider
        mockRepository = MockingFramework.new()
        mockFooDataProvider = mockRepository.CreateMockOfType(FooDataProvider);

        // create a new array of Foo objects
        testFooArray = new Foo[] {Foo.new(), Foo.new(), Foo.new()}

        // the next statement will cause testFooArray to be returned every time we call FooDAtaProvider.GetAllFoos,
        // instead of calling to the database and returning whatever is in there
        // ExpectCallTo and Returns are methods provided by our imaginary mocking framework
        ExpectCallTo(mockFooDataProvider.GetAllFoos).Returns(testFooArray)

        // now begins our actual unit test
        testBar = new Bar(mockFooDataProvider)
        baz = testBar.GetAllFoos()

        // baz should now equal the testFooArray object we created earlier
        Assert.AreEqual(3, baz.length)
    }
}

Un scénario moqueur courant, en un mot. Bien sûr, vous souhaiterez probablement tester également vos appels à la base de données réels, pour lesquels vous devrez accéder à la base de données.

Doug R
la source
Je sais que c'est vieux, mais qu'en est-il de la création d'une table dupliquée à celle qui est déjà dans la base de données. De cette façon, vous pouvez confirmer que les appels DB fonctionnent?
bretterer
1
J'utilise le PDO de PHP comme accès de niveau le plus bas à la base de données, sur laquelle j'ai extrait une interface. Ensuite, j'ai créé une couche de base de données prenant en charge les applications. C'est la couche qui contient toutes les requêtes SQL brutes et d'autres informations. Le reste de l'application interagit avec cette base de données de niveau supérieur. J'ai trouvé que cela fonctionnait assez bien pour les tests unitaires; Je teste mes pages d'application sur la façon dont elles interagissent avec la base de données d'application. Je teste ma base de données d'application sur la façon dont elle interagit avec PDO. Je suppose que PDO fonctionne sans bugs. Code source: manx.codeplex.com
légaliser
1
@bretterer - La création d'une table en double est utile pour les tests d'intégration. Pour les tests unitaires, vous utiliserez généralement un objet simulé qui vous permettra de tester une unité de code quelle que soit la base de données.
BornToCode
2
Quelle est la valeur de la simulation des appels de base de données dans vos tests unitaires? Cela ne semble pas utile car vous pouvez modifier l'implémentation pour renvoyer un résultat différent, mais votre test unitaire réussirait (à tort).
bmay2
2
@ bmay2 Vous n'avez pas tort. Ma réponse originale a été écrite il y a longtemps (9 ans!) Quand beaucoup de gens n'écrivaient pas leur code de manière testable, et quand les outils de test faisaient cruellement défaut. Je ne recommanderais plus cette approche. Aujourd'hui, je voudrais simplement configurer une base de données de test et la remplir avec les données dont j'ai besoin pour le test, et / ou concevoir mon code afin que je puisse tester autant de logique que possible sans aucune base de données.
Doug R
25

Idéalement, vos objets devraient être ignorants persistants. Par exemple, vous devriez avoir une "couche d'accès aux données", à laquelle vous feriez des requêtes, qui renverrait des objets. De cette façon, vous pouvez laisser cette partie en dehors de vos tests unitaires ou les tester de manière isolée.

Si vos objets sont étroitement couplés à votre couche de données, il est difficile d'effectuer des tests unitaires appropriés. la première partie du test unitaire, est "unité". Toutes les unités doivent pouvoir être testées isolément.

Dans mes projets c #, j'utilise NHibernate avec une couche de données complètement séparée. Mes objets vivent dans le modèle de domaine principal et sont accessibles à partir de ma couche d'application. La couche d'application communique à la fois à la couche de données et à la couche de modèle de domaine.

La couche application est également parfois appelée «couche métier».

Si vous utilisez PHP, créez un ensemble spécifique de classes pour UNIQUEMENT accès aux données. Assurez-vous que vos objets n'ont aucune idée de la façon dont ils sont persistants et connectez les deux dans vos classes d'application.

Une autre option serait d'utiliser mocking / stubs.

Sean Chambers
la source
J'ai toujours été d'accord avec cela, mais en pratique en raison des délais et "d'accord, ajoutez maintenant juste une fonctionnalité de plus, avant 14 heures aujourd'hui", c'est l'une des choses les plus difficiles à réaliser. Ce genre de chose est une cible de choix pour la refactorisation, cependant, si mon patron décide un jour qu'il n'a pas pensé à 50 nouveaux problèmes émergents qui nécessitent une toute nouvelle logique et tables métier.
Darren Ringer
3
Si vos objets sont étroitement couplés à votre couche de données, il est difficile d'effectuer des tests unitaires appropriés. la première partie du test unitaire, est "unité". Toutes les unités doivent pouvoir être testées isolément. belle explication
Amitābha
11

Le moyen le plus simple de tester un objet avec un accès à la base de données consiste à utiliser des étendues de transaction.

Par exemple:

    [Test]
    [ExpectedException(typeof(NotFoundException))]
    public void DeleteAttendee() {

        using(TransactionScope scope = new TransactionScope()) {
            Attendee anAttendee = Attendee.Get(3);
            anAttendee.Delete();
            anAttendee.Save();

            //Try reloading. Instance should have been deleted.
            Attendee deletedAttendee = Attendee.Get(3);
        }
    }

Cela rétablira l'état de la base de données, essentiellement comme une annulation de transaction afin que vous puissiez exécuter le test autant de fois que vous le souhaitez sans aucun effet secondaire. Nous avons utilisé cette approche avec succès dans de grands projets. Notre build prend un peu de temps à s'exécuter (15 minutes), mais ce n'est pas horrible d'avoir 1800 tests unitaires. De plus, si le temps de construction est un problème, vous pouvez modifier le processus de construction pour avoir plusieurs versions, une pour la construction de src, une autre qui se déclenche ensuite qui gère les tests unitaires, l'analyse de code, l'empaquetage, etc.

BZ.
la source
1
+1 Gain de temps lors des tests unitaires des couches d'accès aux données. Notez simplement que TS aura souvent besoin de MSDTC, ce qui peut ne pas être souhaitable (selon que votre application aura besoin ou non de MSDTC)
StuartLC
La question originale concernait PHP, cet exemple semble être C #. Les environnements sont très différents.
légaliser
2
L'auteur de la question a déclaré qu'il s'agissait d'une question générale s'appliquant à toutes les langues qui ont quelque chose à voir avec un DB.
Vedran
9
et ce chers amis, est appelé tests d'intégration
AA.
10

Je peux peut-être vous donner un avant-goût de notre expérience lorsque nous avons commencé à examiner les tests unitaires de notre processus de niveau intermédiaire, qui comprenait une tonne d'opérations SQL de «logique métier».

Nous avons d'abord créé une couche d'abstraction qui nous permettait de «insérer» n'importe quelle connexion de base de données raisonnable (dans notre cas, nous avons simplement pris en charge une seule connexion de type ODBC).

Une fois que cela a été mis en place, nous avons pu faire quelque chose comme ça dans notre code (nous travaillons en C ++, mais je suis sûr que vous avez l'idée):

GetDatabase (). ExecuteSQL ("INSERT INTO foo (blah, blah)")

Au moment de l'exécution normal, GetDatabase () renverrait un objet qui alimentait tous nos sql (y compris les requêtes), via ODBC directement dans la base de données.

Nous avons ensuite commencé à examiner les bases de données en mémoire - la meilleure de loin semble être SQLite. ( http://www.sqlite.org/index.html ). C'est remarquablement simple à configurer et à utiliser, et nous a permis de sous-classer et de surcharger GetDatabase () pour transférer SQL vers une base de données en mémoire qui a été créée et détruite pour chaque test effectué.

Nous en sommes encore aux premiers stades, mais cela semble bon jusqu'à présent, mais nous devons nous assurer de créer toutes les tables nécessaires et de les remplir avec des données de test - cependant nous avons quelque peu réduit la charge de travail ici en créant un ensemble générique de fonctions d'assistance qui peuvent faire beaucoup de tout cela pour nous.

Dans l'ensemble, cela a énormément aidé avec notre processus TDD, car apporter ce qui semble être des modifications assez inoffensives pour corriger certains bogues peut avoir des effets assez étranges sur d'autres zones (difficiles à détecter) de votre système - en raison de la nature même de sql / bases de données.

Évidemment, nos expériences se sont centrées sur un environnement de développement C ++, mais je suis sûr que vous pourriez peut-être obtenir quelque chose de similaire fonctionnant sous PHP / Python.

J'espère que cela t'aides.

Alan
la source
9

Vous devez vous moquer de l'accès à la base de données si vous souhaitez tester vos classes unitaire. Après tout, vous ne voulez pas tester la base de données dans un test unitaire. Ce serait un test d'intégration.

Abstenez les appels, puis insérez un simulacre qui renvoie simplement les données attendues. Si vos classes ne font pas plus qu'exécuter des requêtes, cela ne vaut même pas la peine de les tester, cependant ...

Martin Klinke
la source
6

Le livre xUnit Test Patterns décrit quelques façons de gérer le code de test unitaire qui atteint une base de données. Je suis d'accord avec les autres personnes qui disent que tu ne veux pas faire ça parce que c'est lent, mais tu dois le faire un jour, l'OMI. Se moquer de la connexion à la base de données pour tester des éléments de niveau supérieur est une bonne idée, mais consultez ce livre pour des suggestions sur ce que vous pouvez faire pour interagir avec la base de données réelle.

Chris Farmer
la source
4

Options dont vous disposez:

  • Écrivez un script qui effacera la base de données avant de démarrer les tests unitaires, puis remplissez db avec un ensemble de données prédéfini et exécutez les tests. Vous pouvez également le faire avant chaque test - ce sera lent, mais moins sujet aux erreurs.
  • Injectez la base de données. (Exemple en pseudo-Java, mais s'applique à tous les langages OO)

    Base de données de classe {
     Public Result query (String query) {... real db here ...}
    }

    class MockDatabase étend la base de données { Requête de résultat publique (requête de chaîne) { retourne "résultat fictif"; } }

    class ObjectThatUsesDB { public ObjectThatUsesDB (base de données db) { this.database = db; } }

    maintenant, en production, vous utilisez une base de données normale et pour tous les tests, vous injectez simplement la base de données fictive que vous pouvez créer ad hoc.

  • N'utilisez pas du tout DB dans la plupart du code (c'est de toute façon une mauvaise pratique). Créez un objet "base de données" qui, au lieu de retourner avec des résultats, renverra des objets normaux (c'est-à-dire retournera Userau lieu d'un tuple {name: "marcin", password: "blah"}), écrivez tous vos tests avec des objets réels construits ad hoc et écrivez un gros test qui dépend d'une base de données qui assure cette conversion fonctionne bien.

Bien sûr, ces approches ne sont pas mutuellement exclusives et vous pouvez les mélanger et les assortir selon vos besoins.

Marcin
la source
3

Le test unitaire de l'accès à votre base de données est assez facile si votre projet a une cohésion élevée et un couplage lâche. De cette façon, vous pouvez tester uniquement les choses que chaque classe particulière fait sans avoir à tout tester en même temps.

Par exemple, si vous testez l'unité de votre classe d'interface utilisateur, les tests que vous écrivez doivent uniquement essayer de vérifier que la logique à l'intérieur de l'interface utilisateur a fonctionné comme prévu, pas la logique métier ou l'action de base de données derrière cette fonction.

Si vous souhaitez effectuer un test unitaire de l'accès réel à la base de données, vous vous retrouverez en fait avec plus d'un test d'intégration, car vous dépendez de la pile réseau et de votre serveur de base de données, mais vous pouvez vérifier que votre code SQL fait ce que vous lui avez demandé. faire.

Le pouvoir caché des tests unitaires pour moi est que cela me force à concevoir mes applications d'une bien meilleure manière que je ne le pourrais sans eux. C'est parce que cela m'a vraiment aidé à rompre avec la mentalité «cette fonction devrait tout faire».

Désolé, je n'ai pas d'exemple de code spécifique pour PHP / Python, mais si vous voulez voir un exemple .NET, j'ai un article qui décrit une technique que j'ai utilisée pour faire ces mêmes tests.

Toran Billups
la source
2

J'essaie généralement de fractionner mes tests entre le test des objets (et l'ORM, le cas échéant) et le test de la base de données. Je teste le côté objet des choses en se moquant des appels d'accès aux données alors que je teste le côté db des choses en testant les interactions des objets avec la base de données qui est, selon mon expérience, généralement assez limitée.

J'étais frustré par l'écriture de tests unitaires jusqu'à ce que je commence à me moquer de la partie d'accès aux données, donc je n'avais pas à créer une base de données de test ou à générer des données de test à la volée. En vous moquant des données, vous pouvez tout générer au moment de l'exécution et vous assurer que vos objets fonctionnent correctement avec des entrées connues.

akmad
la source
2

Je n'ai jamais fait cela en PHP et je n'ai jamais utilisé Python, mais ce que vous voulez faire est de simuler les appels à la base de données. Pour ce faire, vous pouvez implémenter une certaine IoC, qu'il s'agisse d'un outil tiers ou que vous le gériez vous-même, puis vous pouvez implémenter une version simulée de l'appelant de la base de données qui vous permettra de contrôler le résultat de ce faux appel.

Une forme simple d'IoC peut être réalisée simplement en codant sur Interfaces. Cela nécessite une sorte d'orientation d'objet dans votre code, donc cela peut ne pas s'appliquer à ce que vous faites (je dis que puisque tout ce que je dois continuer, c'est votre mention de PHP et Python)

J'espère que cela vous sera utile, si rien d'autre vous avez des termes à rechercher maintenant.

codeLes
la source
2

Je suis d'accord avec le premier accès post-base de données devrait être dépouillé dans une couche DAO qui implémente une interface. Ensuite, vous pouvez tester votre logique par rapport à une implémentation de stub de la couche DAO.

Chris Marasti-Georg
la source
2

Vous pouvez utiliser des cadres de simulation pour extraire le moteur de base de données. Je ne sais pas si PHP / Python en a mais pour les langages typés (C #, Java etc.) il y a beaucoup de choix

Cela dépend également de la façon dont vous avez conçu ce code d'accès à la base de données, car certaines conceptions sont plus faciles à tester unitaire que d'autres comme celles mentionnées précédemment.

chakrit
la source
2

La configuration des données de test pour les tests unitaires peut être un défi.

En ce qui concerne Java, si vous utilisez les API Spring pour les tests unitaires, vous pouvez contrôler les transactions au niveau de l'unité. En d'autres termes, vous pouvez exécuter des tests unitaires qui impliquent des mises à jour / inserts / suppressions de base de données et annuler les modifications. À la fin de l'exécution, vous laissez tout dans la base de données tel qu'il était avant de commencer l'exécution. Pour moi, c'est aussi bon que possible.

Bino Manjasseril
la source