Search code examples
javaspring-mvcspring-test-mvc

Spring MVC unit test for DeferredResult doesn't call timeout callback


I'm using Spring 4.3.18 and Spring Boot 1.5.14 on Java 7.

I'm implementing a RestController endpoint which returns a DeferredResult with a timeout callback. I'm trying to write a unit test for the timeout callback, but I can't get a MockMvc unit test to call the timeout callback.

For the sake of testing, I wrote this endpoint:

@PostMapping("/test")
public DeferredResult<String>
testit() {
    logger.info("testit called");
    final DeferredResult<String> rv = new DeferredResult<>(1000L);
    rv.onTimeout(new Runnable() {
        @Override
        public void run() {
            logger.info("run called");
            rv.setResult("timed out");
        }
    });
    return rv;
}

and this unit test:

@Autowired
private MockMvc mockMvc;

@Test
public void testTest() throws Exception {
    MvcResult result = mockMvc.perform(post("/rest/tasks/test"))
        .andExpect(request().asyncStarted())
        .andReturn();
    result.getAsyncResult(1500);
    mockMvc.perform(asyncDispatch(result))
        .andExpect(status().isOk())
        ;
}

(The call to result.getAsyncResult(1500) is based on https://jira.spring.io/browse/SPR-16869)

When I run this, the testit() endpoint is called, and after a 1500 ms delay, I get an exception complaining that setResult() was never called. The timeout handler isn't invoked:

java.lang.IllegalStateException: Async result for handler [public org.springframework.web.context.request.async.DeferredResult<java.lang.String> my.package.TasksController.testit()] was not set during the specified timeToWait=1500
    at org.springframework.test.web.servlet.DefaultMvcResult.getAsyncResult(DefaultMvcResult.java:135)
    at my.package.TasksControllerTest.testTest(TasksControllerTest.java:200)
2517 [main] INFO my.package.TasksController - testit called

MockHttpServletRequest:
      HTTP Method = POST
      Request URI = /rest/tasks/test
       Parameters = {}
          Headers = {}

Handler:
             Type = my.package.TasksController
           Method = public org.springframework.web.context.request.async.DeferredResult<java.lang.String> my.package.TasksController.testit()

Async:
    Async started = true
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = {}
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []
4037 [Thread-2] INFO org.springframework.web.context.support.GenericWebApplicationContext - Closing org.springframework.web.context.support.GenericWebApplicationContext@6ea91f6: startup date [Thu Jul 26 19:44:31 EDT 2018]; root of context hierarchy

What do I need to do to make the test framework invoke the timeout handler on the DeferredResult?


Solution

  • It seems it's possible to create a synthetic timeout in the unit test like this:

    @Test
    public void testTest() throws Exception {
        MvcResult result = mockMvc.perform(post("/rest/tasks/test"))
            .andExpect(request().asyncStarted())
            .andReturn();
        // Trigger a timeout on the request
        MockAsyncContext ctx = (MockAsyncContext) result.getRequest().getAsyncContext();
        for (AsyncListener listener : ctx.getListeners()) {
            listener.onTimeout(null);
        }
        mockMvc.perform(asyncDispatch(result))
            .andExpect(status().isOk())
            ;
    }
    

    Accessing the request's async listeners and calling onTimeout() will result in calling the DeferredRequest's timeout callback.

    The call to result.getAsyncResult(1500) from my question is redundant for this test, because asyncDispatch() will call getAsyncResult() anyway.