Affects: 5.1.8


Spring MVC supports controllers return reactive type:

Application code

@RestController
public class DemoController {
    @GetMapping
    public Mono<ResponseEntity<Foobar>> get() {
        return Mono.just(new ResponseEntity<>(new Foobar().setFoo("A").setBar("1"), HttpStatus.ACCEPTED));
    }
}

However, testing this type of application with MockMvc doesn't work.

Test code

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class DemoApplicationTests {

    @Autowired
    private MockMvc client;

    @Test
    public void getRoot() throws Exception {
        client.perform(get("/"))
              .andExpect(status().isAccepted())
              .andExpect(jsonPath("foo").value("A"))
              .andExpect(jsonPath("bar").value("1"))
        ;        
    }
}

Test output

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = []
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

When debugging test:

  • Controller is successfully called
  • Reactive stack and result is properly retrieved
  • ResponseEntity is never processed
  • MockHttpServletResponse fields are never set

When debugging application:

  • It seems request and response are processed by Coyote processor
  • At some future time, DispatcherServlet is called again and a HandlerMethodReturnValueHandler process ResponseEntity

Comment From: loganmzz

After second debugging session, I think there's missing some actions there: https://github.com/spring-projects/spring-framework/blob/v5.1.8.RELEASE/spring-test/src/main/java/org/springframework/test/web/servlet/MockMvc.java#L184

In fact, when Catalina AsyncContext dispatch is called it creates a org.apache.catalina.core.AsyncContextImpl.AsyncRunnable which is called later on by Coyote. In fact, Catalina call ApplicationDispatcher.dispatch(servletRequest, servletResponse)

Then RequestMappingHandlerAdapter detect WebAsyncManager has a concurrent result which resolves result (i.e. ResponseEntity) which is then processed as in synchronous call.

Hope it helps to understand behaviour. I don't know how to fix it properly right now. If I have some ideas, I will try to submit a PR.

Comment From: loganmzz

Ok, I finally get usage of MockMvc in this case of reactive types.

@Test
public void getRoot() throws Exception {
    MvcResult result = client.perform(get("/"))
                             .andExpect(reqest().asyncStarted())
                             .andReturn();
    client.perform(asyncDispatch(result));
          .andExpect(status().isAccepted())
          .andExpect(jsonPath("foo").value("A"))
          .andExpect(jsonPath("bar").value("1"))
    ;
}

Comment From: rstoyanchev

This was also recently documented in #19666.

Comment From: Hassan-Elseoudy

Ok, I finally get usage of MockMvc in this case of reactive types.

@Test public void getRoot() throws Exception { MvcResult result = client.perform(get("/")) .andExpect(reqest().asyncStarted()) .andReturn(); client.perform(asyncDispatch(result)); .andExpect(status().isAccepted()) .andExpect(jsonPath("foo").value("A")) .andExpect(jsonPath("bar").value("1")) ; }

Thank you, this saved me!