You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
mchsamples-.net-core/p08_BDD_EntityFramework/ex_041_004_TestingInMemory
Marc CHEVALDONNE 88ac908fc7
end of .NET5.0 update
2 years ago
..
readmefiles finished sample ex_041_004 5 years ago
Nounours.cs renamed files 5 years ago
NounoursContext.cs renamed files 5 years ago
ReadMe.md end of .NET5.0 update 2 years ago
ex_041_004_TestingInMemory.csproj end of .NET5.0 update 2 years ago

ReadMe.md

ex_041_004_TestingInMemory

02/01/2020 ⋅ Marc Chevaldonné Dernière mise à jour : 09/01/2020 ⋅ Marc Chevaldonné


Lorsqu'on cherche à tester notre code et nos accès à la base de données, on n'a pas nécessairement envie de créer la base juste pour les tests. Pour cela, il existe des solutions et des fournisseurs permettant de tester les bases sans avoir à réellement les créer :

  • le fournisseur InMemory permet ceci mais de manière approximative, car InMemory n'est pas une base de données relationnelle : il y a donc des limitations.
  • SQLite possède un mode In-Memory qui lui, permet de tester une base de données relationnelle, sans avoir à créer une base de données.

Je conseille donc l'utilisation de SQL in Memory plutôt que InMemory, puisqu'il permet de tester une base relationnelle.

Cet exemple montre comment utiliser InMemory et SQLite in-memory à travers une injection de dépendance. En d'autres termes, vous continuez à définir votre chaîne de connexion sur une base de données, mais vous permettez néanmoins l'utilisation, à la demande, de InMemory pour des tests.
Puisque ce fournisseur devient intéressant dans le cas de tests, j'ai donc ajouté un 2ème projet lié à cet exemple, permettant d'avoir accès à des tests unitaires utilisant InMemory ou SQLite in-memory.
Pour le reste de l'exemple, celui-ci n'apporte rien de nouveau par rapport à l'exemple ex_041_001 concernant l'utilisation d'Entity Framework Core.


Pourquoi autant de projets dans cet exemple ?

Quatre projets constituent cet exemple :

  • ex_041_004_TestingInMemory est une bibliothèque de classes .NET Standard contentant le modèle, ie la classe Nounours et la classe NounoursContext
  • ex_041_004_ConsoleTests_w_SqlServer est une application console .NET Core qui prouve le fonctionnement "normal" de la base de données (ie comme dans l'exemple ex_041_001) en exploitant la bibliothèque de classes ex_041_004_TestingInMemory (ne fonctionne que sur Windows)
  • ex_041_004_ConsoleTests_w_SQLite est une application console .NET Core qui prouve le fonctionnement "normal" de la base de données (ie comme dans l'exemple ex_041_001) en exploitant la bibliothèque de classes ex_041_004_TestingInMemory (cross-platform)
  • ex_041_004_UnitTests_w_InMemory est une application de tests unitaires xUnit exploitant le fournisseur InMemory et la bibliothèque de classes ex_041_004_TestingInMemory
  • ex_041_004_UnitTests_w_SQLiteInMemory est une application de tests unitaires xUnit exploitant le fournisseur SQLite in memory et la bibliothèque de classes ex_041_004_TestingInMemory

Vous pouvez donc exécuter cet exemple de quatre manières :

  • via ex_041_004_ConsoleTests_w_SqlServer comme dans l'exemple ex_041_001 avec dotnet ef (seulement sur Windows)
  • via ex_041_004_ConsoleTests_w_SQLite comme dans l'exemple ex_041_001 avec dotnet ef
  • via les tests unitaires de ex_041_004_UnitTests_w_InMemory
  • via les tests unitaires de ex_041_004_UnitTests_w_SQLiteInMemory

Comment a été construit cet exemple ?

bibliothèque .NET Standard ex_041_004_TestingInMemory

Cet exemple est tout d'abord construit de la même manière que l'exemple ex_041_001_ConnectionStrings. Il ne faut pas oublier les NuGet nécessaires :

  • Microsoft.EntityFrameworkCore : pour le projet en général
  • Microsoft.EntityFrameworkCore.SqlServer : pour le provider SQL Server
  • Microsoft.EntityFrameworkCore.Tools : pour bénéficier des outils de Design, de migrations, etc.

J'ai ensuite décidé de renommer ma classe dérivant de DbContext en NounoursContext car je n'ai plus de raison de faire la différence entre SqlServer et SQLite.
On obtient ainsi la classe NounoursContext suivante :

using Microsoft.EntityFrameworkCore;

namespace ex_041_004_TestingInMemory
{
    public class NounoursContext : DbContext
    {
        public DbSet<Nounours> Nounours { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder options)
            => options.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=ex_041_004_TestingInMemory.Nounours.mdf;Trusted_Connection=True;");
    }
}

On la modifie pour permettre d'injecter un autre fournisseur, tout en gardant celui-ci par défaut. On peut utiliser pour cela la propriété IsConfigured sur DbContextOptionsBuilder.
La méthode OnConfiguring de NounoursContext est alors modifiée de la manière suivante :

protected override void OnConfiguring(DbContextOptionsBuilder options)
{
    if (!options.IsConfigured)
    {
        options.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=ex_041_004_TestingInMemory.Nounours.mdf;Trusted_Connection=True;");
    }
}

Dès lors, si rien n'est configuré, c'est donc le fournisseur SqlServer qui sera utilisé.
Mais nous pouvons aussi permettre à l'utilisateur d'en injecter un autre. Pour cela, nous ajoutons deux constructeurs à notre classe :

  • NounoursContext() : le constructeur par défaut ne fait rien, et en conséquence fera que la configuration SqlServer sera utilisée,
  • NounoursContext(DbContextOptions<NounoursContext> options) : ce constructeur par défaut permettra d'injecter une autre fabrique de fournisseur, et permettra à l'utilisateur d'injecter n'importe quel autre fournisseur, dont InMemory.

Les constructeurs injectés sont donc :

public NounoursContext()
{ }

public NounoursContext(DbContextOptions<NounoursContext> options)
    : base(options)
{ }

La classe NounoursContext peut donc s'utiliser comme précédemment, sans changement, et la base SqlServer sera utilisée ; ou alors, on pourra injecter un autre fournisseur.

application console .NET Core ex_041_004_ConsoleTests_w_SqlServer (seulement pour Windows)

L'application console .NET Core ex_041_004_ConsoleTests_w_SqlServer fait référence à la bibliothèque .NET Standard précédente pour pouvoir consommer Nounours et NounoursContext. Sa seule classe est donc Program et peut donc être codée de la manière suivante :

using System;
using ex_041_004_TestingInMemory;

namespace ex_041_004_ConsoleTests_w_SqlServer
{
    class Program
    {
        static void Main(string[] args)
        {
            Nounours chewie = new Nounours { Nom = "Chewbacca" };
            Nounours yoda = new Nounours { Nom = "Yoda" };
            Nounours ewok = new Nounours { Nom = "Ewok" };

            using (var context = new NounoursContext())
            {
                // Crée des nounours et les insère dans la base
                Console.WriteLine("Creates and inserts new Nounours");
                context.Add(chewie);
                context.Add(yoda);
                context.Add(ewok);
                context.SaveChanges();
            }

            using (var context = new NounoursContext())
            {
                foreach(var n in context.Nounours)
                {
                    Console.WriteLine($"{n.Id} - {n.Nom}");
                }
                context.SaveChanges();
            }
        }
    }
}

Pour tester cette application, n'oubliez pas les commandes comme présentées dans l'exemple ex_041_001 : pour générer l'exemple, il vous faut d'abord préparer les migrations et les tables.

  • Ouvrez la Console du Gestionnaire de package, pour cela, dirigez-vous dans le menu Outils, puis Gestionnaire de package NuGet, puis Console du Gestionnaire de package.
  • Dans la console que vous venez d'ouvrir, déplacez-vous dans le dossier du projet .NET Core, ici :
cd .\p08_BDD_EntityFramework\ex_041_004_ConsoleTests_w_SqlServer

Note: si vous n'avez pas installé correctement EntityFrameworkCore, il vous faudra peut-être utiliser également :

  • dotnet tool install --global dotnet-ef si vous utilisez la dernière version de .NET Core (3.1 aujourd'hui),

  • dotnet tool install --global dotnet-ef --version 3.0.0 si vous vous utiliser spécifiquement .NET Core 3.0.

    • Migration : comme la classe dérivant de DbContext n'est pas dans l'application Console, nous devons préciser dans quel projet elle se trouve en ajoutant --project ../ex_041_004_TestingInMemory.
dotnet ef migrations add migration_ex_041_004 --project ../ex_041_004_TestingInMemory
  • Création de la table : comme pour la migration, il faut préciser dans quel projet se trouve l'instance de DbContext.
dotnet ef database update --project ../ex_041_004_TestingInMemory
  • Génération et exécution Vous pouvez maintenant générer et exécuter l'exemple ex_041_004_ConsoleTests_w_SqlServer.

  • Le résultat de l'exécution doit ressembler à :

Creates and inserts new Nounours
1 - Chewbacca
2 - Yoda
3 - Ewok
  • Comment vérifier le contenu des bases de données SQL Server ? Vous pouvez vérifier le contenu de votre base en utilisant l'Explorateur d'objets SQL Server.

  • Pour cela, allez dans le menu Affichage puis Explorateur d'objets SQL Server.

  • Déployez dans l'Explorateur d'objets SQL Server :

    • SQL Server,

    • puis (localdb)\MSSQLLocalDB ...,

    • puis Bases de données

    • puis celle portant le nom de votre migration, dans mon cas : ex_041_004_TestingInMemory.Nounours.mdf

    • puis Tables

    • Faites un clic droit sur la table dbo.Nounours puis choisissez Afficher les données

    • Vous devriez maintenant pouvoir voir les données suivantes dans le tableau :

    Id Nom
    1 Chewbacca
    2 Yoda
    3 Ewok

application console .NET Core ex_041_004_ConsoleTests_w_SQLite

L'application console .NET Core ex_041_004_ConsoleTests_w_SQLite fait référence à la bibliothèque .NET Standard précédente pour pouvoir consommer Nounours et SQLiteNounoursContext. Ses deux seules classes sont donc Program et SQLiteNounoursContext et sont codées de la manière suivante :

using System;
using ex_041_004_TestingInMemory;
using Microsoft.EntityFrameworkCore;

namespace ex_041_004_ConsoleTests_w_SQLite
{
    class Program
    {
        static void Main(string[] args)
        {
            Nounours chewie = new Nounours { Nom = "Chewbacca" };
            Nounours yoda = new Nounours { Nom = "Yoda" };
            Nounours ewok = new Nounours { Nom = "Ewok" };

            using (var context = new SQLiteNounoursContext())
            {
                // Crée des nounours et les insère dans la base
                Console.WriteLine("Creates and inserts new Nounours");
                context.Add(chewie);
                context.Add(yoda);
                context.Add(ewok);
                context.SaveChanges();
            }
        }
    }

    public class SQLiteNounoursContext : NounoursContext
    {
        protected override void OnConfiguring(DbContextOptionsBuilder options)
        {
            if(!options.IsConfigured)
            {
                options.UseSqlite($"Data Source=ex_041_004_SQLite.Nounours.db");
            }
        }
    }
}

La classe SQLiteNounoursContext a pour but de permettre l'appel de dotnet ef sans avoir à utiliser le NounoursContext qui utilise SqlServer. En effet, pour pouvoir mettre à jour la base SQLite, EFCore demande pour le moment, un DbContext correspondant à la base à mettre à jour dans la méthode OnConfiguring.

Pour tester cette application, n'oubliez pas les commandes comme présentées dans l'exemple ex_041_001 : pour générer l'exemple, il vous faut d'abord préparer les migrations et les tables.

  • Ouvrez la Console du Gestionnaire de package, pour cela, dirigez-vous dans le menu Outils, puis Gestionnaire de package NuGet, puis Console du Gestionnaire de package. Ou bien ouvrez le terminal (sous MacOSX)
  • Dans la console ou le terminal que vous venez d'ouvrir, déplacez-vous dans le dossier du projet .NET Core, ici :
cd .\p08_BDD_EntityFramework\ex_041_004_ConsoleTests_w_SQLite

Note: si vous n'avez pas installé correctement EntityFrameworkCore, il vous faudra peut-être utiliser également :

  • dotnet tool install --global dotnet-ef si vous utilisez la dernière version de .NET Core (3.1 aujourd'hui),

  • dotnet tool install --global dotnet-ef --version 3.0.0 si vous vous utiliser spécifiquement .NET Core 3.0.

    • Migration : comme la classe dérivant de DbContext se trouve dans l'application Console, nous n'avons pas à préciser dans quel projet elle se trouve. En revanche, il y a désormais deux contextes (celui d'origine NounoursContext et celui pour SQLite SQLiteNounoursContext), il faut donc préciser le contexte avec --context :
dotnet ef migrations add migration_ex_041_004 --context SQLiteNounoursContext
  • Création de la table :
dotnet ef database update --context SQLiteNounoursContext
  • Génération et exécution Vous pouvez maintenant générer et exécuter l'exemple ex_041_004_ConsoleTests_w_SQLite.

  • Le résultat de l'exécution doit ressembler à :

Creates and inserts new Nounours
1 - Chewbacca
2 - Yoda
3 - Ewok
  • Comment vérifier le contenu des bases de données SQLite ? Pour vérifier le contenu de votre base SQLite, vous pouvez utiliser le programme DB Browser :
  • Rendez-vous sur la page : https://sqlitebrowser.org/dl/ et téléchargez le programme DB Browser.
  • Lancez DB Browser for SQLite
  • Glissez-déposez au milieu de la fenêtre de DB Browser for SQLite le fichier ex_041_004_ConsoleTests_w_SQLite.Nounours.db qui a été généré par l'exécution du programme et qui se trouve près de ex_041_004_ConsoleTests_w_SQLite.csproj. DB Browser for SQLite
  • Choisissez l'onglet Parcourir les données
  • Observez les résultats obtenus DB Browser for SQLite
    • Vous devriez maintenant pouvoir voir les données suivantes dans le tableau :
    Id Nom
    1 Chewbacca
    2 Yoda
    3 Ewok

Configuration des tests unitaires avec InMemory

Le test unitaire est de type xUnit et va permettre d'injecter le fournisseur InMemory.

  • On crée un nouveau projet de tests unitaires (xUnit)
  • On lui ajoute le package NuGet : Microsoft.EntityFrameworkCore.InMemory
  • On ajoute également une référence au projet précédent (ex_041_004_InMemory.exe)
  • On peut ensuite écrire un premier test comme suit :
using ex_041_004_InMemory;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using Xunit;

namespace ex_041_004_UnitTests
{
    public class NounoursDB_Tests
    {
        [Fact]
        public void Add_Test()
        {
            var options = new DbContextOptionsBuilder<NounoursContext>()
                .UseInMemoryDatabase(databaseName: "Add_Test_database")
                .Options;

            //prepares the database with one instance of the context
            using (var context = new NounoursContext(options))
            {
                Nounours chewie = new Nounours { Nom = "Chewbacca" };
                Nounours yoda = new Nounours { Nom = "Yoda" };
                Nounours ewok = new Nounours { Nom = "Ewok" };

                context.Nounours.Add(chewie);
                context.Nounours.Add(yoda);
                context.Nounours.Add(ewok);
                context.SaveChanges();
            }

            //prepares the database with one instance of the context
            using (var context = new NounoursContext(options))
            {
                Assert.Equal(3, context.Nounours.Count());
                Assert.Equal("Chewbacca", context.Nounours.First().Nom);
            }
        }
    }
}

Ce premier test permet d'ajouter 3 nounours et :

  • de vérifier qu'il y a bien trois nounours ajoutés
Assert.Equal(3, context.Nounours.Count());
  • de vérifier que le premier s'appelle bien Chewbacca :
Assert.Equal("Chewbacca", context.Nounours.First().Nom);

Notez que le choix du fournisseur est bien fait au démarrage du test avec la création du DbContextOptionsBuilder :

var options = new DbContextOptionsBuilder<NounoursContext>()
                .UseInMemoryDatabase(databaseName: "Add_Test_database")
                .Options;

et que l'injection est effectuée plus bas :

using (var context = new NounoursContext(options))
{
    //...
}

On peut ensuite ajouter un autre test, par exemple :

[Fact]
public void Modify_Test()
{
    var options = new DbContextOptionsBuilder<NounoursContext>()
        .UseInMemoryDatabase(databaseName: "Modify_Test_database")
        .Options;

    //prepares the database with one instance of the context
    using (var context = new NounoursContext(options))
    {
        Nounours chewie = new Nounours { Nom = "Chewbacca" };
        Nounours yoda = new Nounours { Nom = "Yoda" };
        Nounours ewok = new Nounours { Nom = "Ewok" };

        context.Nounours.Add(chewie);
        context.Nounours.Add(yoda);
        context.Nounours.Add(ewok);
        context.SaveChanges();
    }

    //prepares the database with one instance of the context
    using (var context = new NounoursContext(options))
    {
        string nameToFind = "ew";
        Assert.Equal(2, context.Nounours.Where(n => n.Nom.ToLower().Contains(nameToFind)).Count());
        nameToFind = "ewo";
        Assert.Equal(1, context.Nounours.Where(n => n.Nom.ToLower().Contains(nameToFind)).Count());
        var ewok = context.Nounours.Where(n => n.Nom.ToLower().Contains(nameToFind)).First();
        ewok.Nom = "Wicket";
        context.SaveChanges();
    }

    //prepares the database with one instance of the context
    using (var context = new NounoursContext(options))
    {
        string nameToFind = "ew";
        Assert.Equal(1, context.Nounours.Where(n => n.Nom.ToLower().Contains(nameToFind)).Count());
        nameToFind = "wick";
        Assert.Equal(1, context.Nounours.Where(n => n.Nom.ToLower().Contains(nameToFind)).Count());
    }
}

Ce cas de test :

  • vérifie d'abord qu'il y a deux nounours dont le nom contient la chaîne "ew",
string nameToFind = "ew";
Assert.Equal(2, context.Nounours.Where(n => n.Nom.ToLower().Contains(nameToFind)).Count());
  • vérifie qu'il y a un nounours dont le nom contient la chaîne "ewo"
nameToFind = "ewo";
Assert.Equal(1, context.Nounours.Where(n => n.Nom.ToLower().Contains(nameToFind)).Count());
  • modifie le nom de ce nounours en "Wicket"
var ewok = context.Nounours.Where(n => n.Nom.ToLower().Contains(nameToFind)).First();
ewok.Nom = "Wicket";
  • enregistre les changements
context.SaveChanges();
  • vérifie ensuite qu'il n'y a plus qu'un Nounours dont le nom contient la chaîne "ew"
string nameToFind = "ew";
Assert.Equal(1, context.Nounours.Where(n => n.Nom.ToLower().Contains(nameToFind)).Count());
  • vérifie qu'il y a un Nounours dont le nom contient la chaîne "wick"
nameToFind = "wick";
Assert.Equal(1, context.Nounours.Where(n => n.Nom.ToLower().Contains(nameToFind)).Count());

Configuration des tests unitaires avec SQLite in memory

Le projet se construit exactement da la même manière que le précédent à quelques exceptions près que voici.

  • package NuGet :
    à la place du NuGet Microsoft.EntityFrameworkCore.InMemory, il faut ajouter Microsoft.EntityFrameworkCore.Sqlite
  • ouverture de la connexion :
    au début des tests, il faut penser à ouvrir la connexion avec la base en mémoire SQLite qui doit rester ouverte durant tout le test.
//connection must be opened to use In-memory database
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();
  • avant de commencer à traiter avec la base en mémoire, on peut vérifier qu'elle a bien été créée :
//context.Database.OpenConnection();
context.Database.EnsureCreated();

Au final, les tests ressemblent à :

using ex_041_004_TestingInMemory;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using Xunit;
using Microsoft.Data.Sqlite;


namespace ex_041_004_UnitTests_w_SQLiteInMemory
{
    public class NounoursDB_Tests
    {
        [Fact]
        public void Add_Test()
        {
            //connection must be opened to use In-memory database
            var connection = new SqliteConnection("DataSource=:memory:");
            connection.Open();

            var options = new DbContextOptionsBuilder<NounoursContext>()
                .UseSqlite(connection)
                .Options;

            //prepares the database with one instance of the context
            using (var context = new NounoursContext(options))
            {
                //context.Database.OpenConnection();
                context.Database.EnsureCreated();

                Nounours chewie = new Nounours { Nom = "Chewbacca" };
                Nounours yoda = new Nounours { Nom = "Yoda" };
                Nounours ewok = new Nounours { Nom = "Ewok" };

                context.Nounours.Add(chewie);
                context.Nounours.Add(yoda);
                context.Nounours.Add(ewok);
                context.SaveChanges();
            }

            //uses another instance of the context to do the tests
            using (var context = new NounoursContext(options))
            {
                context.Database.EnsureCreated();
                
                Assert.Equal(3, context.Nounours.Count());
                Assert.Equal("Chewbacca", context.Nounours.First().Nom);
            }
        }

        [Fact]
        public void Modify_Test()
        {
            //connection must be opened to use In-memory database
            var connection = new SqliteConnection("DataSource=:memory:");
            connection.Open();

            var options = new DbContextOptionsBuilder<NounoursContext>()
                .UseSqlite(connection)
                .Options;

            //prepares the database with one instance of the context
            using (var context = new NounoursContext(options))
            {
                //context.Database.OpenConnection();
                context.Database.EnsureCreated();

                Nounours chewie = new Nounours { Nom = "Chewbacca" };
                Nounours yoda = new Nounours { Nom = "Yoda" };
                Nounours ewok = new Nounours { Nom = "Ewok" };

                context.Nounours.Add(chewie);
                context.Nounours.Add(yoda);
                context.Nounours.Add(ewok);
                context.SaveChanges();
            }

            //uses another instance of the context to do the tests
            using (var context = new NounoursContext(options))
            {
                context.Database.EnsureCreated();
               
                string nameToFind = "ew";
                Assert.Equal(2, context.Nounours.Where(n => n.Nom.ToLower().Contains(nameToFind)).Count());
                nameToFind = "wo";
                Assert.Equal(1, context.Nounours.Where(n => n.Nom.ToLower().Contains(nameToFind)).Count());
                var ewok = context.Nounours.Where(n => n.Nom.ToLower().Contains(nameToFind)).First();
                ewok.Nom = "Wicket";
                context.SaveChanges();
            }

            //uses another instance of the context to do the tests
            using (var context = new NounoursContext(options))
            {
                context.Database.EnsureCreated();
             
                string nameToFind = "ew";
                Assert.Equal(1, context.Nounours.Where(n => n.Nom.ToLower().Contains(nameToFind)).Count());
                nameToFind = "wick";
                Assert.Equal(1, context.Nounours.Where(n => n.Nom.ToLower().Contains(nameToFind)).Count());
            }
        }
    }
}

exécution des tests unitaires

Vous pouvez maintenant exécuter les tests unitaires via l'Eplorateur de tests.

  • Dans le menu Test, choisissez Explorateur de tests
  • Cliquez sur "Exécuter tous les tests"
  • Observez le bon fonctionnement

Copyright © 2019-2020 Marc Chevaldonné