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 doReturn
s 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.