javaspring-bootjunit5

Unable to work out why my JUnit Springboot test with MockMVC is expecting a <null> and not a DTO on return from view


I have written a very simple web controller and a very simple unit test with MockMvc.

The web controller returns a DTO of the ScanRequestDTO type to the view with which it populates a form's data.

This mapping is done with the @ModelAttribute tag in the relevant controller method.

When doing the test I want to ensure that am object of the correct type has been supplied in the response to the get request.

However my test continually returns:

Model attribute 'scanRequestDTO' expected: but was:<ScanRequestDTO(name=null, dateOfBirth=null, nationality=null, idNumber=null)>

I am expecting it to be the ScanRequestDTO so I can't work out why the test is expecting null.

The relevant code is:

Controller

@GetMapping("/doscan")
public String doScan(@ModelAttribute("scanRequestDTO") ScanRequestDTO scanRequest, Model model)    
{   
    checkSubmittedData(scanRequest);

    return "doScan";
}

Unit Test

@Test
void doScanTest() throws Exception {
    this.mockMvc.perform(get("/web/doscan"))
        .andExpect(model().attribute("scanRequestDTO", any(ScanRequestDTO.class)))        
        .andExpect(view().name("doScan"))
        .andExpect(status().isOk());
}

The error is coming from .andExpect(model().attribute("scanRequestDTO", any(ScanRequestDTO.class)))

I expect it to pass the test as it is returning a ScanRequestDTO class as expected.

However the test is expecting it to return a null and I cannot for the life of me work out why.


Solution

  • This is due to wrong import of any org.mockito.ArgumentMatchers.any

    The correct one should be org.hamcrest.Matchers.any

    Although the two looks very similar, but how they works are quite different.
    Mockito one is specifically created for Mockito, while Hamcrest one is widely used in different library(including Mockito)

    Why the error look so weird?

    Such misleading error is because there is overloading in ModelResultMatchers

        public <T> ResultMatcher attribute(String name, Matcher<? super T> matcher) {
            return (result) -> {
                ModelAndView mav = this.getModelAndView(result);
                MatcherAssert.assertThat("Model attribute '" + name + "'", mav.getModel().get(name), matcher);
            };
        }
    
        public ResultMatcher attribute(String name, @Nullable Object value) {
            return (result) -> {
                ModelAndView mav = this.getModelAndView(result);
                AssertionErrors.assertEquals("Model attribute '" + name + "'", value, mav.getModel().get(name));
            };
        }
    

    Hence when we imported Mockito's any method, the return type will be ScanRequestDTO, which does not match Matcher<? super T>. So we are actually calling attribute with Object value. And since Mockito any is returning null, the assertion become asserting the attribute to be null.