16

I have several integration tests running on Spring Framework that extend the base class called BaseITCase.
Like this:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {AppCacheConfiguration.class, TestConfiguration.class}, loader = SpringBootContextLoader.class)
@Transactional
@WebMvcTest
public abstract class BaseITCase{...}
...
public class UserControllerTest extends BaseITCase {...}

The problem is that one of the test has several declarations of: @MockBean inside of it and the moment this test executed, Spring recreates context and the tests that follows this one sometimes use wrong beans(from the context created exactly for the test with @MockBean). I found out about that just by checking that beans have different hashcodes.

It becomes really critical when I use @EventListener. Because listeners for wrong context(context of the test class that has already finished execution) are invoked and I have wrong beans there.

Is there any workaround for that?

I tried to move all @MockBean declarations to basic class and it worked fine because new context is not created. But, it makes basic class too heavy. Also, I tried to make a dirty context for this test, but then the next test fails with message that context has already been closed.

dvelopp
  • 3,535
  • 2
  • 26
  • 46

2 Answers2

19

The reason is that the spring configuration for the test having the @MockBean is different from the rest of the tests, so the spring framework cannot cache previously used context and needs to load it again. Here you can find a more detailed explanation: https://github.com/spring-projects/spring-boot/issues/10015

As you said, if you move the mock bean to the parent class the context doesn't get reloaded, which makes sense as the bean configuration remains the same.

A possible workaround is defining your mock bean as a simple mock and injecting it manually where is required.

For instance, UserController has a dependency on 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;
    }
}

Hope this helps.

Daniel Camarasa
  • 316
  • 3
  • 9
0

@MockBean may cause the context to reload as explained in the previous answer.

As an alternative and if you're using spring boot 2.2+, you can use @MockInBean instead of @MockBean. It keeps your context clean and does not require your context to get reloaded.

@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;

}

disclaimer: I created this lib for this exact purpose: mock beans in spring beans and avoid lengthy context recreation.

Antoine Meyer
  • 91
  • 1
  • 2
  • 3