# 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 : ```csharp using Microsoft.EntityFrameworkCore; namespace ex_041_004_TestingInMemory { public class NounoursContext : DbContext { public DbSet 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 : ```csharp 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 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 : ```csharp public NounoursContext() { } public NounoursContext(DbContextOptions 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 : ```csharp 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 : ```csharp 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](./readmefiles/dbbrowser_01.png) * Choisissez l'onglet *Parcourir les données* * Observez les résultats obtenus ![DB Browser for SQLite](./readmefiles/dbbrowser_02.png) * 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 : ```csharp 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() .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 ```csharp Assert.Equal(3, context.Nounours.Count()); ``` * de vérifier que le premier s'appelle bien Chewbacca : ```csharp 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``` : ```csharp var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: "Add_Test_database") .Options; ``` et que l'injection est effectuée plus bas : ```csharp using (var context = new NounoursContext(options)) { //... } ``` On peut ensuite ajouter un autre test, par exemple : ```csharp [Fact] public void Modify_Test() { var options = new DbContextOptionsBuilder() .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", ```csharp 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" ```csharp nameToFind = "ewo"; Assert.Equal(1, context.Nounours.Where(n => n.Nom.ToLower().Contains(nameToFind)).Count()); ``` * modifie le nom de ce nounours en "Wicket" ```csharp var ewok = context.Nounours.Where(n => n.Nom.ToLower().Contains(nameToFind)).First(); ewok.Nom = "Wicket"; ``` * enregistre les changements ```csharp context.SaveChanges(); ``` * vérifie ensuite qu'il n'y a plus qu'un Nounours dont le nom contient la chaîne "ew" ```csharp 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" ```csharp 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. ```csharp //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 : ```csharp //context.Database.OpenConnection(); context.Database.EnsureCreated(); ``` Au final, les tests ressemblent à : ```csharp 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() .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() .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é