Partilhar via


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

Nesta página, discutimos técnicas para escrever testes automatizados que não envolvem o sistema de banco de dados contra o qual o aplicativo é executado em produção, trocando seu banco de dados por um teste duplo . Existem vários tipos de testes duplos e abordagens para fazer isso, e é recomendável ler cuidadosamente Escolhendo uma estratégia de teste para entender completamente as diferentes opções. Finalmente, também é possível testar o seu sistema de base de dados de produção; isto é abordado em Teste o seu sistema de base de dados de produção.

Dica

Esta página mostra técnicas de xUnit, mas conceitos semelhantes existem em outras estruturas de teste, incluindo NUnit.

Padrão do 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 de 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 do EF Core LINQ para uma camada separada, que mais tarde analisaremos ou simularemos. Aqui está um exemplo de uma interface de repositório para o nosso sistema de blogs:

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

    IAsyncEnumerable<Blog> GetAllBlogsAsync();

    void AddBlog(Blog blog);

    Task SaveChangesAsync();
}

... E aqui está uma implementação de exemplo parcial 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 mistério: o repositório envolve simplesmente um contexto EF Core e expõe métodos que executam as consultas e atualizações na base de dados. Um ponto-chave a ser observado é que nosso método GetAllBlogs retorna IAsyncEnumerable<Blog> (ou IEnumerable<Blog>), e não IQueryable<Blog>. Retornar a última opção significaria que os operadores de consulta ainda podem ser compostos sobre o resultado, exigindo que o EF Core ainda seja necessário para a tradução da consulta; isso anulava o propósito de utilizar um repositório em primeiro lugar. IAsyncEnumerable<Blog> permite-nos substituir 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>();

Finalmente, nossos controladores são injetados com o serviço de repositório em vez do contexto 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 é arquitetado de acordo com o padrão do repositório: o único ponto de contato com a camada de acesso a dados - EF Core - agora é através da camada de repositório, que atua como um mediador entre o código do aplicativo e as consultas reais do banco de dados. Os testes agora podem ser escritos simplesmente cortando o repositório ou zombando dele com sua biblioteca de simulação favorita. Aqui está um exemplo de um teste baseado em simulação usando a popular 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 visto aqui.

SQLite na memória

SQLite pode ser facilmente configurado como o provedor EF Core para seu conjunto de testes em vez de seu sistema de banco de dados de produção (e.g. SQL Server); consulte os documentos do provedor SQLite para obter detalhes. No entanto, geralmente é uma boa ideia usar o banco de dados na memória do SQLite recurso durante o teste, uma vez que ele fornece isolamento fácil entre testes e não requer 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 ele é excluído quando essa conexão é fechada. Em uso normal, o DbContext do EF Core abre e fecha 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; assim, como solução alternativa, abrimos a conexão antes de passá-la para o EF Core e providenciamos 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();

Os testes agora podem chamar CreateContext, que retorna um contexto usando a ligação que estabelecemos no construtor, garantindo que tenhamos um banco de dados limpo com os dados inicializados.

O código de exemplo completo para testes na memória SQLite pode ser visualizado aqui.

Provedor em memória

Conforme discutido na página de visão geral do teste , o uso do provedor na memória para teste é fortemente desencorajado; considere usar SQLite em vezou implementar o padrão de repositório. Se você decidiu usar in-memory, 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 visualizado aqui.

Nomenclatura do 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 chamá-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; Embora na maioria dos casos os contextos compartilhem o mesmo provedor de serviços, a configuração de contextos com opções diferentes pode desencadear o uso de um novo provedor de serviços interno. Quando for esse 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 geralmente é feito por ter um campo InMemoryDatabaseRoot estático).

Transações

Note que, por padrão, se uma transação for iniciada, o provedor em memória lançará uma exceção, já que as transações não são suportadas. Em vez disso, pode desejar que as transações sejam ignoradas silenciosamente, configurando o EF Core para IGNORAR InMemoryEventId.TransactionIgnoredWarning, como no exemplo acima. No entanto, se o seu código realmente depende da semântica transacional - por exemplo, depende da reversão realmente revertendo as alterações - seu teste não funcionará.

Visualizações

O provedor in-memory permite a definição de visualizações através de consultas LINQ, usando ToInMemoryQuery:

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