Ескертпе
Бұл бетке кіру үшін қатынас шегін айқындау қажет. Жүйеге кіруді немесе каталогтарды өзгертуді байқап көруге болады.
Бұл бетке кіру үшін қатынас шегін айқындау қажет. Каталогтарды өзгертуді байқап көруге болады.
На этой странице мы обсудим методы написания автоматических тестов, которые не включают систему базы данных, с которой приложение работает в производственной среде, заменив вашу базу данных на тестовый двойник. Существуют различные типы дублей тестов и подходы для этого, и рекомендуется внимательно изучить 'Выбор стратегии тестирования', чтобы полностью понять различные варианты. Наконец, можно также протестировать вашу производственную систему баз данных; это рассматривается в тестировании вашей производственной системы баз данных.
Совет
На этой странице показаны методы 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 в памяти, важно понимать, что новая база данных создается при открытии низкоуровневого подключения и удаляется при закрытии этого подключения. В обычных условиях использования 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 }));