Testando com InMemoryTesting with InMemory

O provedor InMemory é útil quando você deseja testar componentes usando algo que se aproxime da conexão ao banco de dados real, sem a sobrecarga de operações de banco de dados real.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.

Dica

Veja o exemplo deste artigo no GitHub.You can view this article's sample on GitHub.

InMemory não é um banco de dados relacionalInMemory is not a relational database

Provedores de banco de dados do EF Core não precisa ser bancos de dados relacionais.EF Core database providers do not have to be relational databases. InMemory é projetado para ser um banco de dados de uso geral para teste e não foi projetado para simular um banco de dados relacional.InMemory is designed to be a general purpose database for testing, and is not designed to mimic a relational database.

Alguns exemplos incluem:Some examples of this include:

  • InMemory permitirá que você salve os dados que possam violar as restrições de integridade referencial em um banco de dados relacional.InMemory will allow you to save data that would violate referential integrity constraints in a relational database.
  • Se você usar DefaultValueSql(string) para uma propriedade em seu modelo, isso é uma API de banco de dados relacional e não terá efeito ao executar em 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.
  • Simultaneidade por meio da versão de linha/carimbo de hora ([Timestamp] ou IsRowVersion) não tem suporte.Concurrency via Timestamp/row version ([Timestamp] or IsRowVersion) is not supported. Não DbUpdateConcurrencyException será lançada se uma atualização é feita usando um token de simultaneidade antigo.No DbUpdateConcurrencyException will be thrown if an update is done using an old concurrency token.

Dica

Para muitos fins de teste, essas diferenças não serão relevante.For many test purposes these differences will not matter. No entanto, se você quiser testar em relação a algo que se comporta como um verdadeiro banco de dados relacional, considere usar modo na memória do SQLite.However, if you want to test against something that behaves more like a true relational database, then consider using SQLite in-memory mode.

Cenário de teste de exemploExample testing scenario

Considere o seguinte serviço que permite que o código do aplicativo executar algumas operações relacionadas a blogs.Consider the following service that allows application code to perform some operations related to blogs. Ele usa internamente um DbContext que se conecta a um banco de dados do SQL Server.Internally it uses a DbContext that connects to a SQL Server database. Seria útil trocar neste contexto, como se conectar a um banco de dados InMemory para que podemos escrever testes eficientes para esse serviço sem precisar modificar o código ou fazer muito trabalho para criar um teste duplo do contexto.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();
        }
    }
}

Prepare seu contextoGet your context ready

Evite configurar dois provedores de banco de dadosAvoid configuring two database providers

Em seus testes você pretende configurar o contexto para usar o provedor InMemory externamente.In your tests you are going to externally configure the context to use the InMemory provider. Se você estiver configurando um provedor de banco de dados por meio da substituição OnConfiguring em seu contexto, em seguida, você precisará adicionar algum código condicional para garantir que apenas configurar o provedor de banco de dados se um já não tiver sido configurado.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");
    }
}

Dica

Se você estiver usando o ASP.NET Core, em seguida, você não deve precisar esse código, pois o provedor de banco de dados já estiver configurado fora do contexto (em 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).

Adicione um construtor para testeAdd a constructor for testing

A maneira mais simples de habilitar o teste em relação a outro banco de dados é modificar o contexto para expor um construtor que aceita um 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)
    { }

Dica

DbContextOptions<TContext> informa o contexto de todas as suas configurações, como qual banco de dados para se conectar ao.DbContextOptions<TContext> tells the context all of its settings, such as which database to connect to. Isso é o mesmo objeto que é criado, executando o método OnConfiguring em seu contexto.This is the same object that is built by running the OnConfiguring method in your context.

Escrever testesWriting tests

A chave para teste com esse provedor é a capacidade de informar o contexto para usar o provedor InMemory e controlar o escopo do banco de dados na memória.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. Normalmente você deseja limpar um banco de dados para cada método de teste.Typically you want a clean database for each test method.

Aqui está um exemplo de uma classe de teste que usa o banco de dados InMemory.Here is an example of a test class that uses the InMemory database. Cada método de teste especifica um nome de banco de dados exclusivo, que significa que cada método possui seu próprio banco de dados InMemory.Each test method specifies a unique database name, meaning each method has its own InMemory database.

Dica

Para usar o .UseInMemoryDatabase() método de extensão, o pacote NuGet de referência 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());
            }
        }
    }
}