Home > Mobile >  MockBean seemingly being ignored for Scoped Proxy Bean when running tests in series
MockBean seemingly being ignored for Scoped Proxy Bean when running tests in series

Time:04-04

I'm working on unit tests for a Spring Boot MVC Controller with some session-scoped beans, and while the application works as expected, unit tests are proving to be troublesome for an unknown reason and I'm looking to understand a bit more about why.

The bean config looks like this:

@Bean(name="scopedBean")
@Scope(value=WebApplicationContext.SCOPE_SESSION,
    proxyMode=ScopedProxyMode.TARGET_CLASS)
public MyScopedBean myScopedBean() {
    return new MyScopedBean();
}

The controller looks something like this:

@Autowired
private ISomeService service;
@Autowired
private MyScopedBean scopedBean;

@GetMapping("/")
public ModelAndView getRequest(ModelAndView model, @RequestParam("id") UUID id) {

    if(scopedBean.matches(id)){
        //do some stuff
    }
    //return a page

}

There are other methods in the class which get, set and check against values in the scopedBean.

The tests look something like this:

@WebMvcTest(MyController.class)
@Import({ActualValidator.class,SecurityConfig.class})
class MyControllerTest {

    @MockBean
    private MyUserDetailsService userDetailsService;
    @MockBean
    private ISomeService someService;
    @MockBean
    private MyScopedBean scopedBean;

    @Test
    void testA() throws Exception {

        when(scopedBean.getAValue()).thenReturn("myvalue");
        when(scopedBean.matches("someProperty").thenReturn(false);

        mockMvc.perform(get("/"))
            .andExpect(view().name("something");
    }

    @Test
    void testB() throws Exception {

        when(scopedBean.getAnotherValue()).thenReturn("othervalue");
        when(scopedBean.matches("anotherProperty").thenReturn(true);

        mockMvc.perform(get("/"))
            .andExpect(view().name("somethingelse");
    }

}

Each test, when ran individually, passes. When running them all together, they don't just fail, they return null pointers from inside the mocked proxy as if the object had been properly instantiated. I may be misunderstanding the concept of @MockBean but it was my understanding that the object shouldn't be actually instantiated? Is it possible this is because it's an actual object and not an interface?

I've tried configuring a la Baeldung, although the article talks about running as @SpringBootTest rather than a stripped back MVC test. Adding that config as context configuration causes all tests to start returning nulls for redirectedUrls, views, etc. as if it's not being set up correctly - which it probably isn't.

Is there something very obvious that I'm missing as to how my tests could sometimes seemingly be using a non-mocked object?

EDIT:

The plot thickens! Just for kicks, I decided to add a sysout to the start of a few of the controller methods which print out the value of the bean, hoping to get a memory address.

For every test which passes, it prints scopedBean bean.

For the tests which fail, it prints an actual toString a la MyScopedBean(valueA=null, valueB=null, valueC=null) - which seems...wrong. Call me crazy, but that sounds like it's actually creating an object there?

EDIT 2:

I've raised this with the Spring team - at first glance it may look related to this issue, however removing @SessionScope doesn't resolve the problem. Nor does adding mockito-inline. Swapping to @SpyBean results in slightly more consistency - but doReturns are still seemingly ignored.

CodePudding user response:

So the solution is to add mockito-inline for static methods - but unfortunately I'd also made an error further down my controller, which was the real culprit.

Anything created as as @MockBean can still be overridden - so in one of my methods I'd done:

scopedBean = new ScopedBean();

...to "reset" it. Works fine in practice - except when you test, this means that the mock is replaced with a real version of the bean, and because the tests reuse the same context the "mock" will then be useless.

I instead created a method which resets it without reinstantiating. mockito-inline is no longer required.

  • Related