Комментарии 21
Интеграционный тест с использованием @SpringBootTest нужен как минимум для проверки того, что контекст поднимается и нет никаких ошибок в конфигурации.
И тут уже встает вопрос, а если мы всё равно будем поднимать полный контекст, зачем нам дополнительно ещё поднимать отдельные слои контекста?
Потому что при полноценном интегрционном тесте необходимо создавать настоящие данные, чтобы вся цепочка от данных до API корректно отработала. Вместо того чтобы эмулировать поведение среды, которая окружает тестируемый фунционал, её приходится буквально создавать. Тут можно апелировать, что есть же MockBean и т.д., но при его использовании будет поднят отдельный контекст, что ещё больше увеличит время.
Так в вашем примере со слоями тот же MockBean используется.
Я в своих проектах выработал простое решение - везде применяются SpyBean, и объявляются они один раз в базовом классе. Там где нужно - им можно задать поведение, как мокам. Где не нужно - они будут вести себя как настоящие бины. И контекст при этом будет создан только один раз.
необходимо создавать настоящие данные
Можно взять обезличенные и обфусцированные (затираем персональные данные) с прода, которые будут максимально приближены к реальным данным, с которыми придётся работать во время эксплуатации.
Также возникают следующие вопросы:
насколько данные для unit-тестов адекватны и будут соответствовать тому, с чем придётся столкнуться в проде? Не будет ли ситуации, что тестируем одно, а в реальности будет происходить другое?
как избежать ситуации, когда user story или бизнес фича по частям проходит unit-тесты, но в собранном виде уже не работает (а такое нередко случается)?
любой код меняется в ходе жизненного цикла и меняется часто. Как не тратить кучу времени на адаптацию unit-тестов из-за каждого рефакторинга?
Одним из аргументов избегания интеграционных тестов и писать больше unit-тесты является, что unit-тесты быстрее и требуют меньше времени. Если мы говорим, что что-то быстрее, то надо указывать насколько быстрее? На 10 секунд? Допустим, интеграционный тест занимает 10 секунд (чтобы поднять контекст микросервиса и протестировать), unit-тест занимает 5 секунд, то 20 интеграционных тестов потребуют 200 секунд (на самом деле меньше если не злоупотреблять @DirtiesContext), что, следуя вашему аргументу, дольше 2 интеграционных тестов и 40 unit-тестов, которые займут 220 секунд.
Интеграционные тесты переиспользуют контекст (если не злоупотреблять @DirtiesContext), поэтому у нас новый контекст поднимается не на каждый интеграционный тест, а 1-2 раза. Да, это дольше unit-тестов, но вряд ли 10 секунд стоят того.
Обфусцированные и обезличенные данные обычно все же используются для end to end тестов на отдельном стенде, п не в рамках CI/CD. Для обычного пайплайна сборки асе же более характерно поднятие чистой БД в контейнере, ну или H2 на худой конец.
Теперь к тезисам:
Такая ситуация возможна, но это уже зависит от качества проведенного системного анализа.
Очевидно лучше писать тесты) Вы же можете протестировать не только отдельные классы с логикой, но и компонующий их сервис, исходя из того, что его составляющие работают корректно (вы же их протестировали).
Правильные юнит-тесты исподволь заставляют писать менее связанный код, поэтому рефакторинг не такой болезненный как может показаться. Если все таки больно, возможно вы все таки что-то делаете не так.
Юнит тесты отрабатывают за милисекунды.
Как уже ранее писал, тот же MockBean тоже пачкает контекст.
Согласен - лучше писать тесты, интеграционные тесты :) Когда поднимаем микросервис и тестируем его.
Никто не мешает взять часть данных с прода для тестов микросервиса и в рамках pipeline-а. А по остальным пунктам - всё это делается тем, что поднимается микросервис и тестируется.
Можно не использовать MockBean, поднимаем весь микросервис (postgresql, kafka и всё нужно через testcontainers) один 1 раз, а дальше также прогоняем интеграционные тесты за миллисекунды.
У SpringBoot отдельные аннотации для тестирования отдельных слоёв приложения: сериализации JSON, контроллеров MVC, слоя доступа к данным и т. д. Они используются, чтобы весь контекст спринга не поднимать, если я правильно помню.
Описанный вами подход подойдет далеко не всем проектам. Он может быть уместен для домашнего пет-проекта или коробочного коммерческого продукта, который не планируется поддерживать в будущем — сделал и отдал.
Но для продуктовой разработки он совершенно не подходит. Особенность такой разработки — в постоянных изменениях кодовой базы по мере добавления новых функциональностей. Главной задачей разработчика становится поддержание evolvability — способности системы к изменениям. Это означает, что фича одинаковой сложности на первом и пятом году жизни сервиса будет требовать примерно одинаковых усилий. То есть сервис с годами не превращается в монолитный, неподдерживаемый ком.
Ошибочно ставить во главу угла только скорость выполнения тестов — не менее важна и глубина обратной связи. Интеграционный тест дает полноценное представление о работе приложения. Юнит-тест же сообщает лишь о корректности одного из сотен или тысяч классов и ничего не говорит о работоспособности фичи в целом.
Сильный упор на юнит-тесты ведет к цементированию кода: значимый рефакторинг становится дорогим, потому что изменение дизайна тянет за собой переписывание множества тестов. А если тесты меняются следом за кодом, можно ли им доверять? О хрупкости кода при чрезмерном использовании юнит-тестов много пишет Владимир Хориков в книге Принципы юнит-тестирования.
На мой взгляд, при тестировании микросервисов в продуктовой разработке приоритет следует отдавать интеграционным тестам. Вот хорошая статья на эту тему: https://engineering.atspotify.com/2018/01/testing-of-microservices/
Я отчасти согласен с тем, что большое количество юнит тестов затруднит существенный рефакторинг, но это сделают и интеграционные тесты, однако юнит тесты при этом стимулируют писать менее связанный код.
На счёт важности скорости тестов я в корне не согласен, так как от этого зависит скорость обратной связи при разработке, чем быстрее разработчик получает обратную связь, тем легче двигается разработка, особенно в продукте с историей, в котором много логических нюансов и взаимосвязей.
Интеграционные тесты никак не могут помешать рефакторингу, так как они не привязаны ни к дизайну кода, ни к контрактам методов и классов — в отличие от юнит-тестов. То есть интеграционный тест веб-сервиса — это отправить JSON в HTTP-эндпоинт и проверить ответ. У вас полная свобода для рефакторинга: можно даже заменить Java на Kotlin, и грамотно написанный тест вам не помешает, а наоборот — поможет, проверив контракт сервиса.
То, что юнит-тесты якобы способствуют написанию менее связанного кода, само по себе требует подтверждения и не вытекает из самой природы этих тестов.
Теперь о фидбэке от тестов. Уточню, что речь идёт о разработке сервисов в продуктовой разработке, где фичи скорее сквозные — проходят через все слои. Есть два варианта:
Если вы не используете TDD, то обратная связь от тестов игнорируется на большей части разработки. То есть это уже не та обратная связь, о которой идёт речь.
Если вы используете TDD, то до этапа рефакторинга код по определению не имеет устоявшегося дизайна и структуры. Значит, юнит-тесты либо будут мешать — «цементируя» драфтовый код, либо будут фактически превращены в интеграционные.
И вот какой вывод я делаю: если мы всерьёз говорим о важности обратной связи от тестов, то речь, скорее всего, идёт про TDD. А в случае сквозных фичей в продуктовых сервисах только интеграционные тесты позволяют не мешать разработчику писать код, а помогать — проверяя контракт.
Я работал на проекте, где предпочтение отдавалось интеграционным тестам или acceptance, как их там называли. Проблема в таком подходе заключается в том, что эти тесты не проверяют внутреннюю логику и взаимодействие разных модулей. Да, они отлично проверяют контракт. Но если не писать юнит-тесты, то как проверить, что внутри вызывается именно тот модуль, который нужен? В моём случае было очень много багов из-за того, что разработчики писали тесты только под контракт API, не обращая внимание на юнит-тесты.
> эти тесты не проверяют внутреннюю логику и взаимодействие разных модулей
И да и нет. Они не проверяют особенность реализации, но проверяют контракт. Функциональные и нефункциональные требования предъявляются к контракту сервиса. Внутреннее устройство не должно трактовать условия, а служить средством достижения ожидаемого результата. Часто выходит так, что совершенно не важно кто, кого и как вызывает внутри кода, если требования выполняются.
По моему опыту, акцент на интеграционные тесты снижал количество багов и уменьшал time to market. В итоге мы пришли к тому, что сервисы даже не запускались локально для финальной проверки через условный Postman — хватало глубоких интеграционных тестов.
ps: я не утверждаю, что юнит тесты не нужны. Отличное применение им - это проверка маперов, сложной бизнес логики, race condition проверок.
Time to market - да, отлично подходят интеграционные тесты. Проверка на баги? Очень сомнительно. Нужна сильно дисциплинированная команда, но она не всегда есть в наличии. Ну и если уж такой сильный акцент на контракт, то нужен толковый аналитик или управление продуктом, что тоже не всегда в наличии. В общем, должны сойтись звёзды чтобы только на одном интеграционном тестировании вывезти продукт.
70% — юнит-тесты.
25% — slice-тесты.
5% — интеграционные тесты.
Это догма?
Аналогичный вопрос, откуда эти числа, с потолка взяты? Или есть какое-то исследование (и где ссылка на исследование)?
Я вообще считаю, что все эти проценты просто попытка получить какое-то число. Но ведь нужно не число, нужно чтобы тесты реально проверяли то, что нужно проверять. Важно писать тесты, которые проверяют ту часть, которая реально нуждается в проверке. Они должны проверять какую-то фичу, сделанную по задаче, чтобы если разработчик сломал фичу, то у него упал этот тест. Они должны проверять основную логику приложения, чтобы быть уверенным, что приложение ещё работает. Они должны проверять обработку ошибок, чтобы быть уверенным, что ошибки ещё обрабатываются, и их обработка не сломана. И т. д. А просто тест ради покрытия вряд ли принесёт какую-то пользу.
Это совет)
@ExtendWith(MockitoExtension.class), @Mock, @InjectMocks
Я не использую эти аннотации для юнит тестов. Для создания моков использую статический метод mock(MyService.class)
.
Так выглядит типичный класс с юнит тестами
class MyServiceTest {
private final MyRepository mockMyRepo = mock(MyRepository.class);
private final MyService testObject = new MyService(mockMyRepo);
@Test
void test(){
// given
when(mockMyRepo.findById(any(UUID.class))).thenReturn(Optional.Empty());
// when
var result = testObject.findById(UUID.random());
// then
verify(mockMyRepo).findById(any(UUID.com));
assertTrue(result.isEmpty());
}
}
А есть какие нибудь исследования, которые подтверждают не полную бесполезность юнитестов на жаве?
Вот охота статистику глянуть
Пара советов по покрытию тестами проекта на SpringBoot