Можно ли использовать моки (mock objects) для тестирования кода, зависящего от контекста базы данных (например, Entity Framework Core DbContext)?

Ответ

Да, мокирование DbContext — стандартная практика для изоляции модульных тестов от реальной базы данных. Это позволяет тестировать бизнес-логику быстро и детерминировано.

Пример мокирования DbSet с использованием библиотеки Moq:

[Fact]
public void GetCustomer_ReturnsCustomer_WhenExists()
{
    // 1. Arrange (Подготовка)
    var testData = new List<Customer>
    {
        new Customer { Id = 1, Name = "Alice" },
        new Customer { Id = 2, Name = "Bob" }
    }.AsQueryable();

    // Мокируем DbSet<Customer>, чтобы он вел себя как коллекция в памяти
    var mockDbSet = new Mock<DbSet<Customer>>();
    mockDbSet.As<IQueryable<Customer>>().Setup(m => m.Provider).Returns(testData.Provider);
    mockDbSet.As<IQueryable<Customer>>().Setup(m => m.Expression).Returns(testData.Expression);
    mockDbSet.As<IQueryable<Customer>>().Setup(m => m.ElementType).Returns(testData.ElementType);
    mockDbSet.As<IQueryable<Customer>>().Setup(m => m.GetEnumerator()).Returns(testData.GetEnumerator());

    // 2. Мокируем сам DbContext
    var mockContext = new Mock<ApplicationDbContext>();
    mockContext.Setup(c => c.Customers).Returns(mockDbSet.Object);

    // 3. Создаем тестируемый сервис, передавая мок контекста
    var service = new CustomerService(mockContext.Object);

    // 4. Act (Действие) & Assert (Проверка)
    var result = service.GetCustomerById(1);
    Assert.NotNull(result);
    Assert.Equal("Alice", result.Name);
}

Альтернативы и нюансы:

  • In-Memory Database (EF Core): Для тестов, требующих более сложных запросов (с .Include(), .ThenInclude()), иногда проще использовать поставщик Microsoft.EntityFrameworkCore.InMemory. Важно: Его поведение не полностью идентично реальной СУБД.
  • Библиотеки-помощники: Рассмотрите использование библиотек, упрощающих мокирование, например, EntityFrameworkCore.Testing.Moq.
  • Интеграционные тесты: Для проверки корректности SQL-запросов и миграций пишите отдельные интеграционные тесты против тестовой БД (например, SQLite в памяти или локальный Docker-контейнер).

Главный принцип: Мокируйте зависимости, чтобы тестировать свой код, а не работу Entity Framework или базы данных.

Ответ 18+ 🔞

А, ну это ж классика, блядь! Мокировать DbContext — это как надевать презерватив перед тестом: защищает от неожиданных последствий и лишней возни с реальной базой. Тесты тогда летают, а не ползают, как улитка по стеклу.

Смотри, вот как это обычно делают, на примере с этим вашим Moq. Только смотри не усни, а то щас будет магия:

[Fact]
public void GetCustomer_ReturnsCustomer_WhenExists()
{
    // 1. Arrange (Готовим сцену, как в плохом театре)
    var testData = new List<Customer>
    {
        new Customer { Id = 1, Name = "Alice" },
        new Customer { Id = 2, Name = "Bob" }
    }.AsQueryable();

    // Делаем из DbSet куклу, которая будет притворяться коллекцией
    var mockDbSet = new Mock<DbSet<Customer>>();
    mockDbSet.As<IQueryable<Customer>>().Setup(m => m.Provider).Returns(testData.Provider);
    mockDbSet.As<IQueryable<Customer>>().Setup(m => m.Expression).Returns(testData.Expression);
    mockDbSet.As<IQueryable<Customer>>().Setup(m => m.ElementType).Returns(testData.ElementType);
    mockDbSet.As<IQueryable<Customer>>().Setup(m => m.GetEnumerator()).Returns(testData.GetEnumerator());

    // 2. Мокируем сам DbContext — главного актёра, который не умеет играть
    var mockContext = new Mock<ApplicationDbContext>();
    mockContext.Setup(c => c.Customers).Returns(mockDbSet.Object);

    // 3. Кормим наш сервис этой бутафорией
    var service = new CustomerService(mockContext.Object);

    // 4. Act & Assert (Делаем и проверяем, не обосрался ли)
    var result = service.GetCustomerById(1);
    Assert.NotNull(result);
    Assert.Equal("Alice", result.Name);
}

Ну и конечно, есть нюансы, ёпта. Иногда этот Moq такой геморройный, что проще взять In-Memory базу от EF Core. Она, конечно, не настоящая — ведёт себя как сумасшедший родственник на свадьбе, но для простых сценариев сойдёт.

А если совсем заебало эту клоунаду с моками настраивать, есть библиотеки-помощники, которые делают ту же хуйню, но в один клик. Но это уже как дорогой презерватив с рёбрышками — не всем надо.

И главное, запомни: юнит-тесты — это проверить твой код, а не выебать мозги Entity Framework. Для проверки, как там твои запросы в реальной базе исполняются, есть интеграционные тесты. Вот там уже можно развернуть SQLite в памяти или даже Docker-контейнер, и проверить всё по-взрослому.

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