Подделка базы данных(EF + xUnit)
Видео

База данных - внешний ресурс и мы, при тестировании нашего приложения, должны исключить любые намеки на его использование при запуске тестов. 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&amp;> CreateNewContextOptions()
     {
         var serviceProvider = new ServiceCollection()
             .AddEntityFrameworkInMemoryDatabase().BuildServiceProvider();
         var builder = new DbContextOptionsBuilder<DataContext&amp;>();
         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);
    }
 }

Как видим, работать с контекстом в тестах можно точно также, как и в функциональном коде, никаких волшебных приемов тут нет. Каждый раз, когда тест запускается, создается новая виртуальная база данных. База чистая и в ней нет данных (кроме данных, которые в нее поместил тест). Изоляция выполнена, и никто извне не может сломать тест.