Поделиться через


Тестирование без рабочей системы базы данных

На этой странице мы обсудим методы написания автоматических тестов, которые не включают систему базы данных, с которой приложение работает в производственной среде, заменив вашу базу данных на тестовый двойник. Существуют различные типы дублей тестов и подходы для этого, и рекомендуется внимательно изучить 'Выбор стратегии тестирования', чтобы полностью понять различные варианты. Наконец, можно также протестировать вашу производственную систему баз данных; это рассматривается в тестировании вашей производственной системы баз данных.

Совет

На этой странице показаны методы xUnit, но аналогичные понятия существуют в других платформах тестирования, включая NUnit.

Шаблон репозитория

Если вы решили написать тесты без участия в рабочей системе базы данных, рекомендуемый способ для этого — шаблон репозитория; Дополнительные сведения об этом см. в этом разделе. Первым шагом в реализации шаблона репозитория является вынесение запросов EF Core LINQ на отдельный слой, который мы позже можем заглушить или имитировать. Ниже приведен пример интерфейса репозитория для системы блогов:

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

    IAsyncEnumerable<Blog> GetAllBlogsAsync();

    void AddBlog(Blog blog);

    Task SaveChangesAsync();
}

... и вот частичная реализация для использования в рабочей среде:

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...
}

В этом нет ничего сложного: репозиторий просто инкапсулирует контекст объекта EF Core и предоставляет методы, которые выполняют запросы и обновления в базе данных. Важно отметить, что наш метод GetAllBlogs возвращает IAsyncEnumerable<Blog> (или IEnumerable<Blog>), а не IQueryable<Blog>. Возвращение последнего варианта означает, что операторы запросов могут быть составлены для результата, что требует, чтобы EF Core оставался вовлеченным в перевод запроса; это сводит на нет смысл использования репозитория. IAsyncEnumerable<Blog> позволяет нам легко заглушить или издеваться над тем, что возвращает репозиторий.

Для приложения ASP.NET Core необходимо зарегистрировать репозиторий в качестве службы при внедрении зависимостей, добавив следующее в ConfigureServicesприложения:

services.AddScoped<IBloggingRepository, BloggingRepository>();

Наконец, наши контроллеры внедряются в службу репозитория вместо контекста EF Core и выполняют методы в нем:

private readonly IBloggingRepository _repository;

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

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

На этом этапе приложение создается в соответствии с шаблоном репозитория: единственная точка контакта с уровнем доступа к данным — EF Core — теперь через уровень репозитория, который выступает в качестве посредника между кодом приложения и фактическими запросами базы данных. Теперь тесты можно писать просто путем стаббирования репозитория или использования любимой библиотеки для мокинга. Ниже приведен пример теста на основе макета с помощью популярной библиотеки 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);
}

Полный пример кода можно просмотреть здесь.

SQLite в памяти

SQLite можно легко настроить в качестве поставщика EF Core для набора тестов вместо продуктивной базы данных (например, SQL Server); дополнительные сведения см. в документации по поставщику SQLite . Однако обычно рекомендуется использовать функцию базы данных SQLite в памяти при тестировании, так как она обеспечивает простую изоляцию между тестами и не требует работы с фактическими файлами SQLite.

Чтобы использовать SQLite в памяти, важно понимать, что новая база данных создается при открытии низкоуровневого подключения и удаляется при закрытии этого подключения. В обычном использовании DbContext EF Core открывает и закрывает подключения к базе данных по мере необходимости — каждый раз при выполнении запроса, чтобы избежать ненужных операций подключения. Однако с использованием SQLite в памяти это приведет к сбросу базы данных каждый раз; поэтому в качестве обходного решения мы открываем подключение перед передачей его в EF Core и обеспечиваем его закрытие только после завершения теста.

    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();

Теперь тесты могут вызывать CreateContext, который возвращает контекст с использованием подключения, настроенного в конструкторе, гарантируя, что у нас есть чистая база данных с инициализированными данными.

Полный пример кода для тестирования в памяти SQLite можно просмотреть здесь.

Поставщик в оперативной памяти

Как описано на странице обзора тестирования, использование провайдера в оперативной памяти для тестирования настоятельно не рекомендуется; рассмотрите использование SQLite вместоили реализацию паттерна репозитория. Если вы решили использовать базу данных в памяти, ниже приведен типичный конструктор класса тестирования, который настраивает и инициализирует новую базу данных в памяти перед каждым тестом.

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();
}

Полный пример кода для тестирования в памяти можно просмотреть здесь.

Именование базы данных в памяти

Базы данных в памяти идентифицируются простым, строковым именем и можно подключиться к одной базе данных несколько раз, указав то же имя (именно поэтому приведенный выше пример должен вызывать EnsureDeleted перед каждым тестом). Тем не менее обратите внимание, что базы данных в памяти коренятся во внутреннем поставщике услуг контекста; хотя в большинстве случаев контексты совместно используют одного поставщика услуг, настройка контекстов с разными параметрами может активировать использование нового внутреннего поставщика услуг. В этом случае явно передайте один и тот же экземпляр InMemoryDatabaseRoot в UseInMemoryDatabase для всех контекстов, которые должны совместно использовать базы данных в памяти (обычно это делается с помощью статического поля InMemoryDatabaseRoot).

Операции

Обратите внимание, что по умолчанию, если транзакция инициируется, поставщик данных в оперативной памяти выдаст исключение, так как транзакции не поддерживаются. Можно пожелать, чтобы транзакции игнорировались без уведомления, настроив EF Core игнорировать InMemoryEventId.TransactionIgnoredWarning, как показано в приведенном выше примере. Однако если ваш код действительно зависит от семантики транзакций, например, от того, что откат фактически отменяет изменения, ваш тест не будет работать.

Представления

Поставщик данных в оперативной памяти позволяет определять представления с помощью запросов LINQ, используя ToInMemoryQuery.

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