База данных - внешний ресурс и мы, при тестировании нашего приложения, должны исключить любые намеки на его использование при запуске тестов. Entity Framework, в части обеспечения работы с базой данных, также стоит относить к категории вещей, которые в тестах не нуждаются (если у вас все же есть сомнения в том, что EF работает и вы хотите его протестировать... предлагаю не заниматься подобным хотя бы при разработке коммерческого приложения).
Потому наша задача при написании тестов на классы, работающие с базой данных - обеспечить подделку БД. И Core дает для этих целей фантастически удобный инструментарий. Наша сегодняшняя реализация будет основываться на MVC проекте (полагаю, что иные варианты проектов не должны представлять больших проблем после знакомства с базовым подходом).
Общая идея сводится к тому, что мы создаем базу данных в оперативной памяти прямо в момент запуска тестов.
Для реализации описанного ниже подхода необходимо прежде установить рекомендуемые библиотеки.
Настройка тестируемого проекта
Вероятно, для большинства первый шаг очевиден, но все же приведу его для целостности повествования... В файле Startup.cs нашего приложения необходимо прописать ссылку на файл контекста и connection string.
public void ConfigureServices(IServiceCollection services)
{
...
services.AddDbContext<DataContext>(options =>
{
options.UseMySQL(Configuration.GetConnectionString("DataContext"));
});
...
}
В данном случае используется подключение к базе MySql, но вы можете выбрать UseSqlServer или UseSqlite, общая идея не изменится.
Приведу пример реализации DbContext'a, работающего с одной единственной таблицей Article.
public class Article
{
public Article() {}
public Article(string name)
{
Name = name;
}
public int Id { get; set; }
public string Name { get; set; }
}
import Microsoft.EntityFrameworkCore;
public class DataContext : DbContext
{
public DataContext(DbContextOptions<DataContext> options)
: base(options)
{ }
public DbSet<Article> Articles { get; set; }
}
И последний отрывок из функционального кода, описывающий класс, осуществляющий запросы к БД.
public class ArticleRepository : IArticleRepository
{
private readonly DataContext _context;
public ArticleRepository(DataContext context)
{
_context = context;
}
public async Task<bool> Delete(int id)
{
var entity = await _context.Articles.FirstOrDefaultAsync(e => e.Id == id);
if (entity == null) return false;
_context.Articles.Remove(entity);
await _context.SaveChangesAsync();
return true;
}
}
На этом закончим с функциональным кодом и перейдем в тестирующий проект.
Заботимся об изоляции
"Из коробки" реализация поддельной базы данных на поверку вовсе не оказывается "серебряной пулей" и без переделки нарушает один главных принципов автотестирования - изолированность.
При запуске каждого отдельного теста мы хотим, чтобы он работал со своей отдельной поддельной базой данных, не зашумленной его собратьями, но для реализации этого механизма на деле придется кое-что довести напильником...
Существуют различные варианты реализации необходимого функционала с точки зрения организации классов, вы вольны выбирать что угодно, я же в данном случае предпочитаю идти по пути наименьшего сопротивления и реализую в тестирующем проекте статическую фабрику... кто без греха, пусть первым кинет в меня клавиатуру)
На самом деле полагаю этот подход более чем адекватным - ни к чему городить изящные замки там, где совершенно точно хватит глинобитной хижины. Итак...
public static class ContextFactory
{
private static DbContextOptions<DataContext&> CreateNewContextOptions()
{
var serviceProvider = new ServiceCollection()
.AddEntityFrameworkInMemoryDatabase().BuildServiceProvider();
var builder = new DbContextOptionsBuilder<DataContext&>();
builder.UseInMemoryDatabase("db", new InMemoryDatabaseRoot())
.UseInternalServiceProvider(serviceProvider);
return builder.Options;
}
public static DataContext GetContext()
{
return new DataContext(CreateNewContextOptions());
}
}
Тестирование
Вот, собственно и все приготовления, программировать становится проще год от года) Можем писать тесты. Метод мы реализовали всего один, его и проверим.
public class ArticleRepositoryTests
{
private readonly DataContext _context;
private readonly ArticleRepository _repository;
public ArticleRepositoryTests()
{
_context = ContextFactory.GetContext();
_repository = new ArticleRepository(_context);
}
[Fact]
public async Task Delete()
{
var article = new Article("Some name");
_context.Articles.Add(article);
_context.SaveChanges();
//
await _repository.Delete(article.Id);
//
var expectedNull = _context.Articles.FirstOrDefault(e => e.Id == article.Id);
Assert.Null(expectedNull);
}
}
Как видим, работать с контекстом в тестах можно точно также, как и в функциональном коде, никаких волшебных приемов тут нет. Каждый раз, когда тест запускается, создается новая виртуальная база данных. База чистая и в ней нет данных (кроме данных, которые в нее поместил тест). Изоляция выполнена, и никто извне не может сломать тест.