Compartilhar via


Realização de testes sem o 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; para obter mais informações sobre isso, consulte 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. Aqui está um exemplo de uma interface de repositório para nosso sistema de blog:

public interface IBloggingRepository
{
    Task<Blog> GetBlogByNameAsync(string name);

    IAsyncEnumerable<Blog> GetAllBlogsAsync();

    void AddBlog(Blog blog);

    Task SaveChangesAsync();
}

... e aqui está 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 async Task<Blog> GetBlogByNameAsync(string name)
        => await _context.Blogs.FirstOrDefaultAsync(b => b.Name == name);

    // Other code...
}

Não há muito nisso: o repositório simplesmente encapsula um contexto EF Core e expõe métodos que executam as consultas e atualizações do banco de dados nele. Um ponto chave a ser observado é que nosso método GetAllBlogs retorna IAsyncEnumerable<Blog> (ou 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. IAsyncEnumerable<Blog> nos permite fazer stub ou simular facilmente o que o repositório retorna.

Para um aplicativo ASP.NET Core, precisamos registrar o repositório como um serviço na injeção de dependência adicionando o seguinte ao ConfigureServicesdo aplicativo:

services.AddScoped<IBloggingRepository, BloggingRepository>();

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

private readonly IBloggingRepository _repository;

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

[HttpGet]
public async Task<Blog> GetBlog(string name)
    => await _repository.GetBlogByNameAsync(name);

Neste ponto, 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 é 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 async Task GetBlog()
{
    // Arrange
    var repositoryMock = new Mock<IBloggingRepository>();
    repositoryMock
        .Setup(r => r.GetBlogByNameAsync("Blog2"))
        .Returns(Task.FromResult(new Blog { Name = "Blog2", Url = "http://blog2.com" }));

    var controller = new BloggingControllerWithRepository(repositoryMock.Object);

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

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

O código de exemplo completo pode ser exibido aqui.

SQLite na memória

O SQLite pode ser facilmente configurado como o provedor do EF Core para o conjunto de testes em vez do sistema de banco de dados de produção (e.g. SQL Server); consulte os documentos 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 novo banco de dados é criado sempre que uma conexão de baixo nível é aberta e que ela é excluída quando essa conexão é fechada. Em uso normal, o DbContext do EF Core abre e fecha as conexões de banco de dados conforme necessário - sempre que uma consulta é executada - para evitar manter a conexão por tempos desnecessariamente longos. No entanto, com o SQLite na memória, isso levaria à redefinição do banco de dados sempre; portanto, como uma solução alternativa, abrimos a conexão antes de passá-la para o EF Core e organizamos para que ela seja fechada somente quando o teste for concluído:

    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.

O código de exemplo completo para teste na memória do SQLite pode ser exibido 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, aqui está um construtor de classe de teste típico 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();
}

O código de exemplo completo para testes na memória pode ser exibido 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 para todos os contextos que devem compartilhar bancos de dados na memória (isso normalmente é feito por ter um campo de InMemoryDatabaseRoot estático).

Transações

Observe que, por padrão, se uma transação for iniciada, o provedor em memória gerará uma exceção, pois transações não são suportadas. Talvez você deseje 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á.

Visualizaçõ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 }));