Тестирование без системы рабочих баз данных
На этой странице мы обсудим методы написания автоматических тестов, которые не связаны с системой базы данных, с которой приложение выполняется в рабочей среде, путем замены базы данных тестовой двойной. Существуют различные типы тестовых двойних и подходов для этого, и рекомендуется тщательно прочитать стратегию тестирования , чтобы полностью понять различные варианты. Наконец, можно также протестировать систему рабочей базы данных; Это описано в разделе "Тестирование в рабочей системе базы данных".
Совет
На этой странице показаны методы xUnit , но аналогичные понятия существуют в других платформах тестирования, включая NUnit.
Шаблон репозитория
Если вы решили написать тесты без использования рабочей системы базы данных, рекомендуемый способ для этого — шаблон репозитория; Дополнительные сведения об этом см. в этом разделе. Первым шагом реализации шаблона репозитория является извлечение запросов EF Core LINQ на отдельный слой, который мы позже заглушим или макетом. Ниже приведен пример интерфейса репозитория для системы блогов:
public interface IBloggingRepository
{
Blog GetBlogByName(string name);
IEnumerable<Blog> GetAllBlogs();
void AddBlog(Blog blog);
void SaveChanges();
}
... и вот частичная реализация примера для использования в рабочей среде:
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);
Это не так много: репозиторий просто заключает контекст EF Core и предоставляет методы, которые выполняют запросы к базе данных и обновляют их. Ключевым моментом является то, что наш GetAllBlogs метод возвращается IEnumerable<Blog>, а не IQueryable<Blog>. Возврат последнего означает, что операторы запросов по-прежнему могут быть составлены по результатам, требуя, чтобы EF Core по-прежнему принимала участие в переводе запроса; это побеждает цель создания репозитория в первую очередь. IEnumerable<Blog> позволяет нам легко заглушить или издеваться над тем, что возвращает репозиторий.
Для приложения ASP.NET Core необходимо зарегистрировать репозиторий в качестве службы при внедрении зависимостей, добавив в приложение ConfigureServicesследующее:
services.AddScoped<IBloggingRepository, BloggingRepository>();
Наконец, наши контроллеры внедряются в службу репозитория вместо контекста EF Core и выполняют в ней методы:
private readonly IBloggingRepository _repository;
public BloggingControllerWithRepository(IBloggingRepository repository)
=> _repository = repository;
[HttpGet]
public Blog GetBlog(string name)
=> _repository.GetBlogByName(name);
На этом этапе приложение разработано в соответствии с шаблоном репозитория: единственная точка контакта с уровнем доступа к данным — EF Core — теперь осуществляется через уровень репозитория, который выступает в качестве посредника между кодом приложения и фактическими запросами к базе данных. Теперь тесты могут быть написаны просто путем вырезания репозитория или издеваясь над ним с любимой библиотекой макетирования. Ниже приведен пример макетного теста с помощью популярной библиотеки 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);
}
Полный пример кода можно просмотреть здесь.
SQLite в памяти
SQLite можно легко настроить как поставщик EF Core для набора тестов вместо рабочей системы базы данных (например, SQL Server); дополнительные сведения см. в документации по поставщику SQLite. Однако обычно рекомендуется использовать функцию базы данных SQLite в памяти при тестировании, так как она обеспечивает простую изоляцию между тестами и не требует работы с фактическими файлами SQLite.
Чтобы использовать SQLite в памяти, важно понимать, что новая база данных создается при каждом открытии низкоуровневого подключения и что оно удаляет то, что подключение закрыто. При обычном использовании EF Core DbContext открывает и закрывает подключения к базе данных по мере необходимости — каждый раз при выполнении запроса, чтобы избежать ненужных длительных подключений. Однако при использовании 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 пример выше). Однако обратите внимание, что базы данных в памяти коренятся во внутреннем поставщике услуг контекста; хотя в большинстве случаев контексты совместно используют один и тот же поставщик услуг, настройка контекстов с разными параметрами может активировать использование нового внутреннего поставщика услуг. В этом случае явно передайте один и тот же экземпляр InMemoryDatabaseRootUseInMemoryDatabase для всех контекстов, которые должны совместно использовать базы данных в памяти (обычно это делается статическим InMemoryDatabaseRoot полем).
Транзакции
Обратите внимание, что по умолчанию, если транзакция запущена, поставщик в памяти вызовет исключение, так как транзакции не поддерживаются. Вместо этого может потребоваться, чтобы транзакции игнорирулись автоматически, настроив EF Core, чтобы игнорировать InMemoryEventId.TransactionIgnoredWarning их, как показано в приведенном выше примере. Однако если код фактически использует семантику транзакций , например, зависит от отката фактически отката изменений. Тест не будет работать.
Представления
Поставщик в памяти позволяет определение представлений с помощью запросов LINQ с помощью ToInMemoryQuery:
modelBuilder.Entity<UrlResource>()
.ToInMemoryQuery(() => context.Blogs.Select(b => new UrlResource { Url = b.Url }));