Какой подход вы применяете для покрытия тестами кода, работающего с базой данных?

«Какой подход вы применяете для покрытия тестами кода, работающего с базой данных?» — вопрос из категории Тестирование, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

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

1. Модульные тесты (Изоляция)

Использую моки (Mock) или стабы (Stub) для абстракций доступа к данным (например, IRepository). Это самый быстрый и надежный способ.

Пример с Moq (C#):

[Fact]
public void GetUser_ShouldReturnUser()
{
    // Arrange
    var mockRepo = new Mock<IUserRepository>();
    var expectedUser = new User { Id = 1, Name = "Test" };
    mockRepo.Setup(repo => repo.GetById(1)).Returns(expectedUser);
    var service = new UserService(mockRepo.Object);

    // Act
    var result = service.GetUser(1);

    // Assert
    Assert.Equal(expectedUser, result);
    mockRepo.Verify(repo => repo.GetById(1), Times.Once); // Проверяем вызов
}

2. Интеграционные тесты (Работа с БД)

Здесь нужна реальная или приближенная к реальной СУБД.

  • InMemory Provider (EF Core): Быстро, но неполноценно. Подходит только для простой проверки логики, не имитирует специфичные для БД функции (ограничения, транзакции, индексы).
    var options = new DbContextOptionsBuilder<AppDbContext>()
        .UseInMemoryDatabase(databaseName: "TestDb")
        .Options;
  • Локальная файловая БД (SQLite): Лучшая эмуляция, особенно в режиме in-memory. Поддерживает многие функции реальной СУБД.
    var connection = new SqliteConnection("DataSource=:memory:");
    connection.Open();
    var options = new DbContextOptionsBuilder<AppDbContext>()
        .UseSqlite(connection)
        .Options;
  • TestContainers (Docker): Золотой стандарт для интеграционных тестов. Запускает реальную БД (PostgreSQL, MySQL) в Docker-контейнере. Максимально приближено к production.
    // Пример на Java
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");

3. Ключевые практики

  • Изоляция тестов: Каждый тест должен работать со своим набором данных. Использую транзакции с откатом (TransactionScope) или очистку БД перед каждым тестом.
  • Идемпотентность: Тесты должны давать одинаковый результат при многократном запуске.
  • Отдельная тестовая БД: Никогда не тестирую на production или shared-базе. Использую отдельный инстанс, управляемый через инфраструктуру как код.