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

Ответ

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

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-базе. Использую отдельный инстанс, управляемый через инфраструктуру как код.

Ответ 18+ 🔞

А, ну это же классическая дилемма, блядь! Ты сидишь, думаешь: как бы так протестировать, чтобы и быстро было, и чтоб потом на проде не вылезло, что ты мудак, который про транзакции забыл. Ёпта, история старая, как мир.

Смотри, тут всё просто, как три копейки. Всё упирается в то, что ты тестируешь. Если у тебя чистая бизнес-логика, оторванная от всего — ты её и тестируй в вакууме, как космонавта. Зачем ей реальная база? Она её нихуя не знает. Берёшь моки, подсовываешь ей фейковые данные и смотришь, не обосрётся ли она. Это как проверить, работает ли двигатель, не заводя всю машину.

Вот смотри, пример, чтоб понятнее было. Допустим, у тебя сервис, который юзеров ищет. Ну и что он делает? Ходит в репозиторий. А репозиторий — это просто интерфейс, абстракция, болтушка. Так давай ему в рот такую болтушку и засунем, какую нам надо!

[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); // И главное — проверим, а обращались ли к нашей болтушке ровно один раз? А то мало ли, она там ещё куда-нибудь позвонила!
}

Видишь? Никакой базы. Чистая симуляция. Быстро, как угорелый. Это для модульных тестов — самое то.

Но, блядь, это же полкартины! А если твоя логика завязана на каких-нибудь хитрожопых JOIN'ах, оконных функциях, или там ограничения целостности на уровне базы? Вот тут моки тебе как мёртвому припарка. Ты протестируешь свою красивую логику, запустишь на прод, а там тебе база: «А я на такое согласия не давала!» И пиздец.

Для этого нужны интеграционные тесты. Тут уже надо базу эмулировать. И есть варианты, от дешёвых до пафосных.

  1. InMemory Provider в EF Core. Это как тренироваться воевать на детском батуте. Быстро, мягко, безопасно. Но, сука, он нихуя не повторяет поведение реальной базы. У него нет уникальных индексов, каскадного удаления, сложных типов данных. Он для простейших сценариев: «вставил — считал». Использовать можно, но понимая, что это игрушка.

  2. SQLite в памяти. Вот это уже серьёзнее. Почти настоящая SQL-база, только в оперативке. Много чего умеет, многие сценарии можно проверить. Отличный компромисс между скоростью и адекватностью. Многие на нём и останавливаются.

  3. TestContainers (через Docker). А это, блядь, тяжёлая артиллерия. Золотой стандарт, ебать его в сраку. Ты говоришь: «Хочу PostgreSQL 16-й версии». И он тебе в Docker'е поднимает самый настоящий постгрес, с погонами и усами. Ты против него тесты гоняешь, и ты на 99% уверен, что на проде будет так же. Это максимально близко к бою. Но и возни с ним, конечно, овердохуища.

И главное, запомни, как «Отче наш»:

  • Изоляция тестов: Каждый тест — как первоклассник с собственной партой. Один насрал в свою базу — другому не пахнет. Используй транзакции с откатом или перед каждым тестом вычищай таблицы так, чтобы блестело.
  • Идемпотентность: Тест должен быть как кнопка. Нажал сто раз — получил один и тот же результат. Не должно быть: «Ой, а первый раз прошёл, а второй — нет, потому что данные уже есть».
  • Отдельная тестовая БД: Это святое. Если ты запускаешь тесты на общей или, упаси боже, продакшен-базе — ты не программист, ты вандал и террорист. Тестовая база должна жить своей жизнью и умирать по твоей команде, не затрагивая никого вокруг.

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