Использование @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 и избежать длительного воссоздания контекста.

Помимо вышеуказанных решений, если вы хотите внедрить их повсюду, вы можете

  1. Создайте конфигурацию в своих тестовых пакетах и ​​определите макетные компоненты как @Primary, чтобы они были внедрены вместо реальных.

            @Configuration
     public class MockClientConfiguration {
    
       @Bean
       @Primary
       public ApiClient mockApiClient() {
         return mock(ApiClient.class);
       }
    
  2. В вашем базовом тестовом классе @Autowire, поскольку они @Primary, вы получите макеты. Обратите внимание, они защищены

    @SpringBootTest общественный класс BaseIntTest {

               @Autowired
       protected ApiClient mockApiClient;
    
  3. Затем в базовом тестовом классе вы можете сбросить макеты перед каждым запуском и установить поведение по умолчанию:

    @BeforeEach public void setup() { Mockito.reset(mockApiClient);Mockito.when(mockApiClient.something(USER_ID)).thenReturn(true); }

  4. Из ваших тестовых классов можно получить доступ к макетам:

            public class MyTest extends BaseIntTest {
       @Test
       public void importantTestCase() {
           Mockito.reset(mockApiClient);
           Mockito.when(mockApiClient.something(USER_ID)).thenReturn(false);
    
Другие вопросы по тегам