Как организовать базу данных для тестов контроллера в C#
Что проверяют
Различаете ли вы юнит-тест контроллера и интеграционный тест через весь HTTP-конвейер и знаете ли, почему провайдер InMemory плохой выбор для второго. Следом спрашивают, как изолировать тесты друг от друга и как создаётся схема.
Как отвечать
Сначала разведите уровни:
- юнит-тест: контроллер тонкий и зависит от сервиса, поэтому базу не поднимают вовсе. Сервис подменяют через Moq или NSubstitute и проверяют, что контроллер вернул нужный
IActionResultили код ответа; - интеграционный тест:
WebApplicationFactory<Program>изMicrosoft.AspNetCore.Mvc.Testingподнимает приложение в памяти, запросы идут черезHttpClient, работают маршрутизация, привязка модели, фильтры и авторизация. Строка подключения подменяется вConfigureWebHostилиConfigureTestServices.
Для базы в интеграционных тестах выбор такой:
- Testcontainers: пакеты
Testcontainers.PostgreSqlилиTestcontainers.MsSqlзапускают настоящую СУБД в Docker. Контейнер поднимают один раз на коллекцию тестов черезIAsyncLifetimeи общую фикстуру, а не на каждый тест; - SQLite в памяти: быстрее, но это другой диалект SQL. Соединение с
DataSource=:memory:нужно держать открытым, иначе база исчезнет; - схема:
Database.MigrateAsync(), а неEnsureCreated(), тогда заодно проверяются миграции.
Изоляция: чистить таблицы между тестами библиотекой Respawn, оборачивать тест в транзакцию с откатом или давать каждому классу свою базу. Данные создавайте в самом тесте через билдеры, а не общим сидом на все случаи.
Где спотыкаются
Берут UseInMemoryDatabase. Microsoft сама не рекомендует его для тестов: он не реляционный, не проверяет внешние ключи и уникальные индексы, не поддерживает транзакции и сырой SQL. Тест зелёный, а на проде запрос падает.
Используют одно имя in-memory базы на все тесты. Данные одного теста видны другому, и результат зависит от порядка запуска.
Создают new MyController(context) и называют это интеграционным тестом. Маршруты, валидация модели и фильтры при этом не участвуют.