Teste sem o sistema de banco de dados de produção

Nesta página, abordaremos técnicas para escrever testes automatizados que não envolvem o sistema de banco de dados no qual o aplicativo é executado na produção, trocando o seu banco de dados por um dublê de teste. Existem vários tipos de dublês de teste e abordagens para fazer isso, e é recomendável ler atentamente Escolher uma estratégia de teste para entender bem as diferentes opções. Por fim, também é possível testar em seu sistema de banco de dados de produção. Isso é abordado em Testes em sistema de banco de dados de produção.

Dica

Esta página mostra as técnicas do xUnit, mas há conceitos semelhantes em outras estruturas de teste, como o NUnit.

Padrão de Repositório

Se você decidiu escrever testes sem envolver seu sistema de banco de dados de produção, a técnica recomendada para fazer isso é o padrão do repositório e para obter mais informações sobre isso, confira esta seção. A primeira etapa da implementação do padrão de repositório é extrair suas consultas LINQ do EF Core para uma camada separada, as quais mais tarde faremos stub ou simularemos. Confira um exemplo de uma interface de repositório para nosso sistema de blog:

public interface IBloggingRepository
{
    Blog GetBlogByName(string name);

    IEnumerable<Blog> GetAllBlogs();

    void AddBlog(Blog blog);

    void SaveChanges();
}

... e esta aqui é uma implementação parcial de exemplo para uso em produção:

public class BloggingRepository : IBloggingRepository
{
    private readonly BloggingContext _context;

    public BloggingRepository(BloggingContext context)
        => _context = context;

    public Blog GetBlogByName(string name)
        => _context.Blogs.FirstOrDefault(b => b.Name == name);

    // Other code...
}

Não é nada de mais: o repositório simplesmente encapsula um contexto do EF Core e expõe métodos que executam as consultas e atualizações do banco de dados nele. Um ponto importante a ser observado é que nosso método GetAllBlogs retorna IEnumerable<Blog> e não IQueryable<Blog>. Retornar este último significaria que os operadores de consulta ainda podem ser compostos sobre o resultado, exigindo que o EF Core ainda esteja envolvido na conversão da consulta e isso frustraria a finalidade de ter um repositório antes de tudo. IEnumerable<Blog> nos permite fazer stub ou simular facilmente o que o repositório retorna.

Em um aplicativo ASP.NET Core, é necessário registrar o repositório como um serviço na injeção de dependência adicionando o seguinte ao ConfigureServices do aplicativo:

services.AddScoped<IBloggingRepository, BloggingRepository>();

Por fim, o serviço de repositório é inserido em nossos controladores em vez do contexto do EF Core e nossos controladores executam métodos nele:

private readonly IBloggingRepository _repository;

public BloggingControllerWithRepository(IBloggingRepository repository)
    => _repository = repository;

[HttpGet]
public Blog GetBlog(string name)
    => _repository.GetBlogByName(name);

Neste momento, seu aplicativo é projetado de acordo com o padrão do repositório: o único ponto de contato com a camada de acesso a dados – EF Core – agora é feito por meio da camada do repositório, que atua como um mediador entre o código do aplicativo e as consultas de banco de dados reais. Agora, os testes podem ser escritos simplesmente realizando o stub do repositório ou simulando eles com sua biblioteca de simulações preferida. Aqui está um exemplo de um teste baseado em simulação usando a famosa biblioteca Moq:

[Fact]
public void GetBlog()
{
    // Arrange
    var repositoryMock = new Mock<IBloggingRepository>();
    repositoryMock
        .Setup(r => r.GetBlogByName("Blog2"))
        .Returns(new Blog { Name = "Blog2", Url = "http://blog2.com" });

    var controller = new BloggingControllerWithRepository(repositoryMock.Object);

    // Act
    var blog = controller.GetBlog("Blog2");

    // Assert
    repositoryMock.Verify(r => r.GetBlogByName("Blog2"));
    Assert.Equal("http://blog2.com", blog.Url);
}

Veja o código de exemplo completo aqui.

SQLite na memória

O SQLite pode ser facilmente configurado como o provedor EF Core para seu conjunto de testes em vez do sistema de banco de dados de produção (por exemplo, SQL Server); confira a Documentação do provedor SQLite para obter detalhes. No entanto, geralmente é aconselhável usar o recurso de banco de dados na memória do SQLite ao fazer testes, pois ele fornece isolamento fácil entre testes e não exige lidar com arquivos SQLite reais.

Para usar o SQLite na memória, é importante entender que um banco de dados novo é criado sempre que uma conexão de baixo nível é aberta e que ele é excluído quando essa conexão é fechada. Em uso normal, o EF Core abre e fecha as conexões de banco de dados DbContext conforme necessário, sempre que uma consulta é executada, para evitar manter a conexão por períodos desnecessariamente longos. No entanto, com o SQLite na memória, isso levaria à redefinição do banco de dados todas as vezes, portanto, uma solução alternativa seria abrir a conexão antes de passá-la para o EF Core e providenciar o fechamento dela somente após a conclusão do teste:

    public SqliteInMemoryBloggingControllerTest()
    {
        // Create and open a connection. This creates the SQLite in-memory database, which will persist until the connection is closed
        // at the end of the test (see Dispose below).
        _connection = new SqliteConnection("Filename=:memory:");
        _connection.Open();

        // These options will be used by the context instances in this test suite, including the connection opened above.
        _contextOptions = new DbContextOptionsBuilder<BloggingContext>()
            .UseSqlite(_connection)
            .Options;

        // Create the schema and seed some data
        using var context = new BloggingContext(_contextOptions);

        if (context.Database.EnsureCreated())
        {
            using var viewCommand = context.Database.GetDbConnection().CreateCommand();
            viewCommand.CommandText = @"
CREATE VIEW AllResources AS
SELECT Url
FROM Blogs;";
            viewCommand.ExecuteNonQuery();
        }

        context.AddRange(
            new Blog { Name = "Blog1", Url = "http://blog1.com" },
            new Blog { Name = "Blog2", Url = "http://blog2.com" });
        context.SaveChanges();
    }

    BloggingContext CreateContext() => new BloggingContext(_contextOptions);

    public void Dispose() => _connection.Dispose();

Agora, os testes podem chamar CreateContext, o que retorna um contexto usando a conexão que configuramos no construtor, garantindo que tenhamos um banco de dados limpo com os dados propagados.

Veja o código de exemplo completo para o teste na memória do SQLite aqui.

Provedor na memória

Conforme abordado na página de visão geral do testes, o uso do provedor na memória para testes é extremamente não recomendado. Em vez disso, considere usar o SQLite ou implementar o padrão do repositório. Se você decidiu usar na memória, confira um construtor de classe de teste comum que configura e propaga um novo banco de dados na memória antes de cada teste:

public InMemoryBloggingControllerTest()
{
    _contextOptions = new DbContextOptionsBuilder<BloggingContext>()
        .UseInMemoryDatabase("BloggingControllerTest")
        .ConfigureWarnings(b => b.Ignore(InMemoryEventId.TransactionIgnoredWarning))
        .Options;

    using var context = new BloggingContext(_contextOptions);

    context.Database.EnsureDeleted();
    context.Database.EnsureCreated();

    context.AddRange(
        new Blog { Name = "Blog1", Url = "http://blog1.com" },
        new Blog { Name = "Blog2", Url = "http://blog2.com" });

    context.SaveChanges();
}

Veja o código de exemplo completo para testes na memória aqui.

Nomenclatura de banco de dados na memória

Os bancos de dados na memória são identificados por um nome de cadeia de caracteres simples e é possível se conectar ao mesmo banco de dados várias vezes, fornecendo o mesmo nome, (por isso que o exemplo acima deve chamar EnsureDeleted antes de cada teste). No entanto, observe que os bancos de dados na memória estão enraizados no provedor de serviços interno do contexto, enquanto, na maioria dos casos, os contextos compartilham o mesmo provedor de serviços, a configuração de contextos com opções diferentes pode disparar o uso de um novo provedor de serviços interno. Quando esse for o caso, passe explicitamente a mesma instância de InMemoryDatabaseRoot para UseInMemoryDatabase em todos os contextos que devem compartilhar bancos de dados na memória (normalmente, isso é feito ao obter um campo InMemoryDatabaseRoot estático).

Transactions

Observe que, por padrão, se uma transação for iniciada, o provedor na memória gerará uma exceção, pois não há suporte para as transações. Você pode ter transações silenciosamente ignoradas, configurando o EF Core para ignorar InMemoryEventId.TransactionIgnoredWarning como no exemplo acima. No entanto, se o código realmente depender da semântica transacional, por exemplo, dependendo da reversão de alterações de fato, o teste não funcionará.

Exibições

O provedor na memória permite a definição de exibições por meio de consultas LINQ usando ToInMemoryQuery:

modelBuilder.Entity<UrlResource>()
    .ToInMemoryQuery(() => context.Blogs.Select(b => new UrlResource { Url = b.Url }));