Test avec InMemoryTesting with InMemory

Le fournisseur en mémoire est utile lorsque vous souhaitez tester les composants à l’aide d’un élément qui est une approximation de la connexion à la base de données réelle, sans la surcharge des opérations de base de données réelle.The InMemory provider is useful when you want to test components using something that approximates connecting to the real database, without the overhead of actual database operations.

Conseil

Vous pouvez afficher cet exemple sur GitHub.You can view this article's sample on GitHub.

En mémoire n’est pas une base de données relationnelleInMemory is not a relational database

Fournisseurs de base de données EF Core n’ont pas à être des bases de données relationnelles.EF Core database providers do not have to be relational databases. InMemory est conçu pour être une base de données à usage général pour le test et n’est pas conçu pour imiter une base de données relationnelle.InMemory is designed to be a general purpose database for testing, and is not designed to mimic a relational database.

Voici quelques exemples de ce sont :Some examples of this include:

  • InMemory vous permettra enregistrer les données qui risque de violer les contraintes d’intégrité référentielle dans une base de données relationnelle.InMemory will allow you to save data that would violate referential integrity constraints in a relational database.
  • Si vous utilisez DefaultValueSql(string) pour une propriété dans votre modèle, cela est une API de base de données relationnelle et n’a aucun effet lors de l’exécution par rapport à InMemory.If you use DefaultValueSql(string) for a property in your model, this is a relational database API and will have no effect when running against InMemory.
  • Accès concurrentiel via la version de l’horodatage/ligne ([Timestamp] ou IsRowVersion) n’est pas pris en charge.Concurrency via Timestamp/row version ([Timestamp] or IsRowVersion) is not supported. Ne DbUpdateConcurrencyException sera levée si une mise à jour est effectuée à l’aide d’un ancien jeton d’accès concurrentiel.No DbUpdateConcurrencyException will be thrown if an update is done using an old concurrency token.

Conseil

Pour autant à des fins de test ces différences ne seront pas important.For many test purposes these differences will not matter. Toutefois, si vous souhaitez tester par rapport à quelque chose qui se comporte davantage comme une véritable base de données relationnelle, envisagez d’utiliser mode in-memory de SQLite.However, if you want to test against something that behaves more like a true relational database, then consider using SQLite in-memory mode.

Exemple de scénario de testExample testing scenario

Envisagez le service suivant qui permet au code d’application effectuer certaines opérations liées aux blogs.Consider the following service that allows application code to perform some operations related to blogs. Il utilise en interne un DbContext qui se connecte à une base de données SQL Server.Internally it uses a DbContext that connects to a SQL Server database. Il serait utile pour le remplacement de ce contexte pour se connecter à une base de données en mémoire afin que nous pouvons écrire des tests efficaces pour ce service sans avoir à modifier le code, ou vous pouvez faire beaucoup de travail pour créer un test double du contexte.It would be useful to swap this context to connect to an InMemory database so that we can write efficient tests for this service without having to modify the code, or do a lot of work to create a test double of the context.

using System.Collections.Generic;
using System.Linq;

namespace BusinessLogic
{
    public class BlogService
    {
        private BloggingContext _context;

        public BlogService(BloggingContext context)
        {
            _context = context;
        }

        public void Add(string url)
        {
            var blog = new Blog { Url = url };
            _context.Blogs.Add(blog);
            _context.SaveChanges();
        }

        public IEnumerable<Blog> Find(string term)
        {
            return _context.Blogs
                .Where(b => b.Url.Contains(term))
                .OrderBy(b => b.Url)
                .ToList();
        }
    }
}

Préparer votre contexteGet your context ready

Évitez de configurer deux fournisseurs de base de donnéesAvoid configuring two database providers

Dans vos tests, vous vous apprêtez à configurer en externe le contexte pour utiliser le fournisseur en mémoire.In your tests you are going to externally configure the context to use the InMemory provider. Si vous configurez un fournisseur de base de données en substituant OnConfiguring dans votre contexte, vous devez ensuite ajouter du code conditionnel afin de configurer le fournisseur de base de données uniquement si elle n’a pas déjà été configurée.If you are configuring a database provider by overriding OnConfiguring in your context, then you need to add some conditional code to ensure that you only configure the database provider if one has not already been configured.

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    if (!optionsBuilder.IsConfigured)
    {
        optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFProviders.InMemory;Trusted_Connection=True;ConnectRetryCount=0");
    }
}

Conseil

Si vous utilisez ASP.NET Core, vous devez inutile ce code dans la mesure où votre fournisseur de base de données est déjà configuré en dehors du contexte (dans Startup.cs).If you are using ASP.NET Core, then you should not need this code since your database provider is already configured outside of the context (in Startup.cs).

Ajoutez un constructeur pour le testAdd a constructor for testing

Pour activer le test par rapport à une autre base de données le plus simple consiste à modifier votre contexte pour exposer un constructeur qui accepte un DbContextOptions<TContext>.The simplest way to enable testing against a different database is to modify your context to expose a constructor that accepts a DbContextOptions<TContext>.

public class BloggingContext : DbContext
{
    public BloggingContext()
    { }

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

Conseil

DbContextOptions<TContext> Indique le contexte à tous ses paramètres, tels que la base de données pour se connecter à.DbContextOptions<TContext> tells the context all of its settings, such as which database to connect to. Il s’agit de l’objet qui est généré en exécutant la méthode OnConfiguring dans votre contexte.This is the same object that is built by running the OnConfiguring method in your context.

Écriture de testsWriting tests

La clé à tester avec ce fournisseur est la possibilité d’indiquer le contexte d’utiliser le fournisseur en mémoire et de contrôler l’étendue de la base de données en mémoire.The key to testing with this provider is the ability to tell the context to use the InMemory provider, and control the scope of the in-memory database. En général, vous souhaitez une nouvelle base de données pour chaque méthode de test.Typically you want a clean database for each test method.

Voici un exemple d’une classe de test qui utilise la base de données en mémoire.Here is an example of a test class that uses the InMemory database. Chaque méthode de test spécifie un nom de base de données unique, ce qui signifie que chaque méthode possède sa propre base de données en mémoire.Each test method specifies a unique database name, meaning each method has its own InMemory database.

Conseil

Pour utiliser le .UseInMemoryDatabase() référence le package NuGet, méthode d’extension Microsoft.EntityFrameworkCore.InMemory.To use the .UseInMemoryDatabase() extension method, reference the NuGet package Microsoft.EntityFrameworkCore.InMemory.

using BusinessLogic;
using Microsoft.EntityFrameworkCore;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Linq;

namespace TestProject.InMemory
{
    [TestClass]
    public class BlogServiceTests
    {
        [TestMethod]
        public void Add_writes_to_database()
        {
            var options = new DbContextOptionsBuilder<BloggingContext>()
                .UseInMemoryDatabase(databaseName: "Add_writes_to_database")
                .Options;

            // Run the test against one instance of the context
            using (var context = new BloggingContext(options))
            {
                var service = new BlogService(context);
                service.Add("http://sample.com");
            }

            // Use a separate instance of the context to verify correct data was saved to database
            using (var context = new BloggingContext(options))
            {
                Assert.AreEqual(1, context.Blogs.Count());
                Assert.AreEqual("http://sample.com", context.Blogs.Single().Url);
            }
        }

        [TestMethod]
        public void Find_searches_url()
        {
            var options = new DbContextOptionsBuilder<BloggingContext>()
                .UseInMemoryDatabase(databaseName: "Find_searches_url")
                .Options;

            // Insert seed data into the database using one instance of the context
            using (var context = new BloggingContext(options))
            {
                context.Blogs.Add(new Blog { Url = "http://sample.com/cats" });
                context.Blogs.Add(new Blog { Url = "http://sample.com/catfish" });
                context.Blogs.Add(new Blog { Url = "http://sample.com/dogs" });
                context.SaveChanges();
            }

            // Use a clean instance of the context to run the test
            using (var context = new BloggingContext(options))
            {
                var service = new BlogService(context);
                var result = service.Find("cat");
                Assert.AreEqual(2, result.Count());
            }
        }
    }
}