Всем привет.

Данная статья не о том как правильно писать тесты , а что делать если внезапно стали "моргать" тесты

В ходе работы над проектом в «АльфаСтрахование» пишем проект на микро–сервисах, и сложилось так, что один из "микро-сервисов" сильно разросся (но до монолита ему ещё далеко :) ). Жили мы так долго и счастливо, пока не стали "переезжать" в облако, тут и начались приключения.

Переезд ничем особенно не запомнился для команды разработки, только вопросами от DevOps насчёт портов и т.д. Замечу, что все интеграционные тесты мы выпилили для того, чтобы отвязаться от зависимости от других команд, когда у них что-то падает на тестовых стендах. Но стала происходить "магия" в JUnit тестах, а именно – стали падать тесты. Падали они фантомно и непредсказуемо, лечилось до поры до времени это retraem pipeline, до тех пор пока эта проблема не стала блокером для выкладок изменений.

 Тест 1 запуск первый.
Тест 1 запуск первый.

Дальше просто retraem.

 Тест 1 запуск второй.
Тест 1 запуск второй.

И так можно было "крутить рулетку" долго и упорно.

Стали разбираться (спасибо понимающему бизнесу за то, что дали на это время). Так примерно выглядели заголовки наших тестов (которых было ооочень много, так как мы используем Sonar).

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class ContractStatusServiceTest {
    @Autowired
    private ContractStatusService contractStatusService;
    @MockBean
    private RsaInfoComponent rsaInfoComponent;
    @MockBean
    private ContractRepository contractRepository;

Давайте разберём "магические" аннотации

  1. @RunWith(SpringJUnit4ClassRunner.class) -  запуск контейнера Spring для выполнения модульного теста.

  2. @SpringBootTest - аннотация говорит Spring Boot пойти и найти основной класс конфигурации (например, с @SpringBootApplication) и использовать его для запуска контекста приложения Spring. SpringBootTest загружает полное приложение.

  3. @Autowired - Инжектит Bean;

И закралась мысль, а почему мы используем @Autowired, ведь его строго не рекомендуют использовать при разработке приложений, так как есть риск, что он не успеет проинжектится в поле.

Начались эксперименты такого рода.

@RunWith(SpringRunner.class)
@SpringBootTest
@RequiredArgsConstructor
public class  ComponentTestTest {

   // @Autowired
    private final ComponentTest componentTest;
    

То есть попытка проинжектить бин как в приложение, через конструктор:

1)@RequiredArgsConstructor - аннотация Lombok для автоматического создания конструкторов из полей final.

Но.....

java.lang.Exception: Test class should have exactly one public zero-argument constructor

	at org.junit.runners.BlockJUnit4ClassRunner.validateZeroArgConstructor(BlockJUnit4ClassRunner.java:171)
	at org.junit.runners.BlockJUnit4ClassRunner.validateConstructor(BlockJUnit4ClassRunner.java:148)
	at org.junit.runners.BlockJUnit4ClassRunner.collectInitializationErrors(BlockJUnit4ClassRunner.java:127)
	...

А жаль.

Дальше стало приходит осознание, а зачем мы поднимаем контекст всего приложения для простых тестов, и вспомнили про Mock.

@RunWith(MockitoJUnitRunner.class)
public class CrossProductServiceTest {
    @InjectMocks
    private CrossProductService crossProductService;
    @Mock
    private KaskoService kaskoService;
    @Mock
    private CrownVirusOfferService crownVirusOfferService;

Давайте разберёмся, что тут происходит и в чём принципиальная разница.

  1. @RunWith(MockitoJUnitRunner.class) - заполняет заглушками наш Bean, а не поднимается контекст (подробнее можно почитать в  доках ).

  2. @Mock -сама заглушка.

  3. @InjectMocks - создаёт Bean и передаёт в конструктор заглушки.

И всё "звалось".

Плюсы :

1.    У нас в разы ускорились тесты при деплоях (так как контекст не поднимается).

2.    Мы перестали ловить и бояться не проинжектеных бинов.

Минусы:

1.    Пришлось переписывать многие тесты.