Использование @MockBean в тестах приводит к перезагрузке контекста приложения.
У меня есть несколько интеграционных тестов, работающих на Spring Framework, которые расширяют базовый класс BaseITCase.
Как это:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {AppCacheConfiguration.class, TestConfiguration.class}, loader = SpringBootContextLoader.class)
@Transactional
@WebMvcTest
public abstract class BaseITCase{...}
...
public class UserControllerTest extends BaseITCase {...}
Проблема состоит в том, что в одном из тестов есть несколько объявлений: @MockBean внутри него, и в момент выполнения этого теста Spring воссоздает контекст, а в тестах, которые следуют этому тесту, иногда используются неправильные компоненты (из контекста, созданного именно для теста с @MockBean). Я узнал об этом, просто проверив, что бины имеют разные хэш-коды.
Это становится действительно критичным, когда я использую @EventListener. Потому что слушатели для неправильного контекста (контекст тестового класса, который уже завершил выполнение) вызываются, и у меня там неправильные компоненты.
Есть ли обходной путь для этого?
Я попытался переместить все объявления @MockBean в базовый класс, и он работал нормально, потому что новый контекст не создан. Но это делает базовый класс слишком тяжелым. Кроме того, я попытался создать грязный контекст для этого теста, но затем следующий тест завершается неудачно с сообщением о том, что контекст уже закрыт.
3 ответа
Причина в том, что конфигурация пружины для теста, имеющего @MockBean, отличается от остальных тестов, поэтому среда пружины не может кэшировать ранее использованный контекст и нуждается в его повторной загрузке. Здесь вы можете найти более подробное объяснение: https://github.com/spring-projects/spring-boot/issues/10015
Как вы сказали, если вы перемещаете фиктивный бин в родительский класс, контекст не перезагружается, что имеет смысл, так как конфигурация бина остается прежней.
Возможный обходной путь - определить ваш фиктивный боб как простой макет и ввести его вручную, где это необходимо.
Например, UserController
имеет зависимость от Foo
:
public class UserControllerTest extends BaseITCase {
private Foo foo = Mockito.mock(Foo.class);
@Autowired
private UserController userController;
@Before
public void setUp() {
super.setup();
this.userController.setFoo(foo);
}
}
@Component
public class UserController {
private Foo foo;
@Autowired
public void setFoo(final Foo foo) {
this.foo = foo;
}
}
Надеюсь это поможет.
может вызвать перезагрузку контекста, как описано в предыдущем ответе .
В качестве альтернативы, и если вы используете Spring boot 2.2+, вы можете использовать @MockInBean вместо
@MockBean
. Он сохраняет ваш контекст в чистоте и не требует перезагрузки контекста.
@SpringBootTest
public class UserControllerTest extends BaseITCase {
@MockInBean(UserController.class)
private Foo foo;
@Autowired
private UserController userController;
@Test
public void test() {
userController.doSomething();
Mockito.verify(foo).hasDoneSomething();
}
}
@Component
public class UserController {
@Autowired
private Foo foo;
}
отказ от ответственности: я создал эту библиотеку именно для этой цели: имитировать beans в Spring beans и избежать длительного воссоздания контекста.
Помимо вышеуказанных решений, если вы хотите внедрить их повсюду, вы можете
Создайте конфигурацию в своих тестовых пакетах и определите макетные компоненты как @Primary, чтобы они были внедрены вместо реальных.
@Configuration public class MockClientConfiguration { @Bean @Primary public ApiClient mockApiClient() { return mock(ApiClient.class); }
В вашем базовом тестовом классе @Autowire, поскольку они @Primary, вы получите макеты. Обратите внимание, они защищены
@SpringBootTest общественный класс BaseIntTest {
@Autowired protected ApiClient mockApiClient;
Затем в базовом тестовом классе вы можете сбросить макеты перед каждым запуском и установить поведение по умолчанию:
@BeforeEach public void setup() { Mockito.reset(mockApiClient);Mockito.when(mockApiClient.something(USER_ID)).thenReturn(true); }
Из ваших тестовых классов можно получить доступ к макетам:
public class MyTest extends BaseIntTest { @Test public void importantTestCase() { Mockito.reset(mockApiClient); Mockito.when(mockApiClient.something(USER_ID)).thenReturn(false);