Комментарии 2
Поддерживаю написанное выше, на нашем проекте со временем пришли к тем же выводам. Дополню несколько интересных моментов.
Использовать Spring-контекст гарантированно один раз за прогон всех тестов помогает вот такая простая штука:
@Slf4j
public class FailFastContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
private static final AtomicBoolean flag = new AtomicBoolean();
@SuppressFBWarnings("DM_EXIT")
@Override
public void initialize(@NonNull ConfigurableApplicationContext applicationContext) {
boolean error = flag.getAndSet(true);
if (error) {
// throw new IllegalStateException("Second application context start attempt.");
log.error("""
Second application context start attempt.
####################################################
# SECOND APPLICATION CONTEXT START ATTEMPT. #
# ----------------------------------------- #
# Please look upper for the reason why Spring is #
# trying to recreate test context. This can be #
# caused by adding an @Import/@MockBean and some #
# other annotations to the test class. #
# #
# This process will now exit immediately. #
####################################################
""");
System.exit(-1);
}
}
}
Нужно только подложить его в @ContextConfiguration(initializers={...})
на главном тестовом классе приложения BaseApplicationTest (от которого удобно наследовать все непосредственно тестовые классы).
Вызовы своего боевого API совершаем через честный http-клиент. Это даёт небольшие накладные расходы на сериализацию/десериализацию, зато дополнительно покрывает: конфигурацию Jackson (кто не ловил, что Instant сериализуется в массив чисел?), цепочку фильтров (особенно если подложены свои фильтры), настройки приложения (вкл/выкл OSiV?) и т.п. В общем, успешно пройденный тест даёт ещё больше гарантий, чем вызов напрямую контроллеров.
Иногда (в полной мере этого слова) — всё-таки приходится искать способы внедрить какие-то ассерты в кишки боевого кода. Для этого можно придумать отдельный интерфейс, похожий на обычные функциональные (Function, Consumer, Supplier, в зависимости от вашей ситуации), и инжектить в боевой код Optional<ThisInterface> и дёргать в нужном месте. Естественно, в боевом контексте не будет реализации и ничего выполняться не будет. В тестах подкладывать собственную реализацию этого интерфейса, инжектить в свой тестовый класс, использовать. Обычно внутри реализации прячется какой-нибудь Exchange, AtomicReference или какой-то такой примитив для обмена данными.
Не стесняйтесь в тестовом коде писать свои контроллеры, сервисы и даже репозитории. Все они могут помочь не строить костыли. Или можно написать просто класс-accessor для package-private данных в каком-то боевом пакете, просто расположив его в нём же и сделав публичным. Сделать какой-то боевой код (классы, методы) package-private вместо private — может быть приемлемым компромиссом в ряде случаев. Я тогда в комментариях так и пишу "Является package-private для целей тестирования".
Ну и самая пушка в нашем проекте — гарантия от тестовой инфраструктуры в сторону теста, что БД всегда чиста. Требует некоторое время для реализации, даёт некоторые накладные расходы, зато как приятно ощущать, что тест действительно изолирован от остальных. Очень радует и при написании новых, и при исправлении имеющихся.
В заключении хочется повторить Uncle Боба — относитесь к коду своих тестов также, как к боевому, и ни в коем случае не как к коду второго сорта.
Спасибо за совет. Громкий ЛОГ при выходе действительно хорошо подсвечивает то, что несколько запусков по прохождении тестов.
Под тестами контроллеров имеются в виду тесты @MockMvc - они полностью имитируют запрос и ответ к нему. Обращение к методу контроллера происходит с помощью URL.
Модульные тесты как оплот стабильности в Agile разработке