У меня есть два класса интеграционных тестов. Один из этих классов зависит от bean-компонента, который взаимодействует с внешним сервисом, поэтому мне нужно смоделировать этот bean-компонент, и @MockBean
кажется идеальным для этого. Для ввода некоторых семян в БД я использую flyway
's afterMigrate.sql
. Итак, вот как это выглядит:
@RunWith(SpringRunner.class)
@ActiveProfiles("test")
@SpringBootTest
@Transactional
@Rollback
class FooTest {
@Autowired
private MyService myService;
}
@RunWith(SpringRunner.class)
@ActiveProfiles("test")
@SpringBootTest
@Transactional
@Rollback
class BarTest {
@MockBean
private ExternalService;
@Autowired
private MyService myService;
}
И afterMigrate.sql
:
INSERT INTO my_table (id, name) VALUES (1, 'John Doe')
Проблема появилась, когда я аннотировал ExternatService
как @MockBean
, так как теперь afretMigrate.sql
запускается дважды, и я получаю сообщение об ошибке:
java.lang.IllegalStateException: Failed to load ApplicationContext
....
Message : ERROR: duplicate key value violates unique constraint "my_table_pkey"
Когда я меняю @MockBean
на @Autowired
, ошибка исчезает, и контекст создается без проблем. Также тесты проходят без проблем, если я запускаю BarTest
отдельно.
Это не ожидаемое поведение для @MockBean
, как говорится в документации:
Any existing single bean of the same type defined in the context will be replaced by the mock. If no existing bean is defined a new one will be added. Dependencies that are known to the application context but are not beans (such as those registered directly) will not be found and a mocked bean will be added to the context alongside the existing dependency.
Это не говорит о том, что контекст будет воссоздан.
Потому что, когда вы используете аннотацию @MockBean
, ваш контекст будет загружаться для каждого теста. См. эта проблема с гитхабом. Цитата с этой страницы:
The Spring test framework will cache an ApplicationContext whenever possible between test runs. In order to be cached, the context must have an exactly equivalent configuration. Whenever you use @MockBean, you are by definition changing the context configuration.
Поэтому, когда вы используете свой фиктивный компонент в разных тестах, контекст будет воссоздаваться каждый раз для вашего тестового класса. Итак, если у вас, например, есть компоненты, которые загружают данные в БД при создании контекста, например, компоненты для пролетного пути, они будут создаваться каждый раз при воссоздании контекста.
Вот как я решил эту проблему (которую я считаю проблемой).
Решение 1:
Я создал класс MockConfig
, который должен создать один mock
для всего набора тестов:
@Configration
public class MockConfig {
@Bean
@Primary
public ExternalService externalService() {
return mock(ExternalService.class);
}
}
И в тесте я просто автоматически подключаю внешнюю службу:
@Autowire
private ExternalService externalService;
Но у этого решения есть проблема: оно создаст настоящий bean-компонент, а затем переопределит его фиктивным bean-компонентом. Если ваша внешняя служба подключается к внешним ресурсам при создании, а вам это не нужно, вам понадобится другое решение.
Решение 2:
Создайте базовый абстрактный класс с @MockBean
в нем:
@RunWith(SpringRunner.class)
@ActiveProfiles("test")
@SpringBootTest
@Transactional
@Rollback
public abstract class BaseIntegrationTest {
@MockBean
ExternalService externalService;
}
И расширите интеграционный тест из этого базового класса:
class FooTest extends BaseIntegrationTest {
@Autowired
private MyService myService;
}
class BarTest extends BaseIntegrationTest {
@Autowired
private MyService myService;
}
Теперь контекст не будет обновляться, так как он всегда один и тот же, и настоящий компонент не будет создан.