Question

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 aux bases de données.

Était-ce utile?

La solution

Je suggérerais de vous moquer de vos appels à la base de données.Les simulations 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.disponible pour l'appelant.Mais au lieu d’effectuer l’action pour laquelle ils sont programmés lorsqu’une méthode particulière est appelée, il 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 qu'ils soient simulés, vous devrez 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 simulation 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 toujours tester unitairement vos appels de base de données réels, pour lesquels vous devrez accéder à la base de données.

Autres conseils

Idéalement, vos objets devraient être ignorants et persistants.Par exemple, vous devriez avoir une "couche d'accès aux données", à laquelle vous adresseriez 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 "unitaire".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 depuis ma couche d'application.La couche application communique à la fois avec la couche de données et la couche de modèle de domaine.

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

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

Une autre option serait d'utiliser des mocking/stubs.

Le moyen le plus simple de tester un objet avec accès à la base de données consiste à utiliser les é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 pour 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 builds, une pour la construction de src, une autre qui se déclenche ensuite et gère les tests unitaires, l'analyse de code, l'empaquetage, etc...

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

Résumez les appels, puis insérez une simulation qui renvoie simplement les données attendues.Si vos classes ne font pas plus que exécuter des requêtes, cela ne vaut peut-être même pas la peine de les tester...

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 a permis d'« insérer » toute connexion raisonnable à une base de données (dans notre cas, nous avons simplement pris en charge une seule connexion de type ODBC).

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

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

Au moment de l'exécution normale, GetDatabase() renverrait un objet qui alimentait tout notre SQL (y compris les requêtes), via ODBC directement à la base de données.

Nous avons ensuite commencé à examiner les bases de données en mémoire - la meilleure semble être de loin SQLite.(http://www.sqlite.org/index.html).Il est remarquablement simple à configurer et à utiliser, et nous a permis de sous-classer et de remplacer 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 tout semble bien jusqu'à présent, mais nous devons nous assurer de créer tous les tableaux requis 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é notre processus TDD, car apporter des modifications qui semblent 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 concentrées sur un environnement de développement C++, mais je suis sûr que vous pourriez peut-être faire fonctionner quelque chose de similaire sous PHP/Python.

J'espère que cela t'aides.

Le livre Modèles de test xUnit 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 vous ne voulez pas faire ça parce que c'est lent, mais vous devrez le faire un jour, 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 obtenir des suggestions sur les choses que vous pouvez faire pour interagir avec la base de données réelle.

Options dont vous disposez :

  • Écrivez un script qui effacera la base de données avant de démarrer les tests unitaires, puis remplissez la base de données 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)

    class Database {
     public Result query(String query) {... real db here ...}
    }

    class MockDatabase étend la base de données {public Result Query (String Query) {return "Mock Result";}}

    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 renvoyer des résultats, renverra des objets normaux (c'est-à-direreviendra User au lieu d'un tuple {name: "marcin", password: "blah"}) écrivez tous vos tests avec des construits ad hoc réel objets et écrivez un gros test qui dépend d'une base de données qui garantit que cette conversion fonctionne correctement.

Bien entendu, ces approches ne s’excluent pas mutuellement et vous pouvez les mélanger selon vos besoins.

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

J'étais frustré par l'écriture de tests unitaires jusqu'à ce que je commence à me moquer de la partie accès aux données afin de ne pas avoir à 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.

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

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

J'espère que cela vous sera utile, au moins vous avez quelques termes sur lesquels effectuer une recherche maintenant.

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

Vous pourriez utiliser cadres moqueurs 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 le code d'accès à la base de données, car certaines conceptions sont plus faciles à tester unitairement que d'autres, comme l'ont mentionné les articles précédents.

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

Par exemple, si vous testez unitairement 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, et non la logique métier ou l'action de la base de données derrière cette fonction.

Si vous souhaitez tester unitairement l'accès réel à la base de données, vous vous retrouverez en réalité avec davantage de tests d'intégration, car vous dépendrez 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 personnellement est qu'ils m'obligent à concevoir mes applications d'une bien meilleure manière que je ne le ferais 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'exemples de code spécifiques pour PHP/Python, mais si vous voulez voir un exemple .NET, j'ai un poste qui décrit une technique que j'ai utilisée pour faire ce même test.

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/insertions/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 démarrer l'exécution.Pour moi, c'est aussi bon que possible.

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