How to test DeferredResult timeoutResult

2019-01-25 07:45发布

问题:

I'm implementing long polling as per the Spring blog from some time ago.

Here my converted method with same response signature as before, but instead of responding immediately, it now uses long polling:

private Map<String, DeferredResult<ResponseEntity<?>>> requests = new ConcurrentHashMap<>();

@RequestMapping(value = "/{uuid}", method = RequestMethod.GET)
public DeferredResult<ResponseEntity<?>> poll(@PathVariable("uuid") final String uuid) {
    // Create & store a new instance
    ResponseEntity<?> pendingOnTimeout = ResponseEntity.accepted().build();
    DeferredResult<ResponseEntity<?>> deferredResult = new DeferredResult<>(TWENTYFIVE_SECONDS, pendingOnTimeout);
    requests.put(uuid, deferredResult);

    // Clean up poll requests when done
    deferredResult.onCompletion(() -> {
        requests.remove(deferredResult);
    });

    // Set result if already available
    Task task = taskHolder.retrieve(uuid);
    if (task == null)
        deferredResult.setResult(ResponseEntity.status(HttpStatus.GONE).build());
    else
        // Done (or canceled): Redirect to retrieve file contents
        if (task.getFutureFile().isDone())
            deferredResult.setResult(ResponseEntity.created(RetrieveController.uri(uuid)).build());

    // Return result
    return deferredResult;
}

In particular I'd like to return the pendingOnTimeout response when the request takes too long (which I returned immediately before), to prevent proxies from cutting off the request.

Now I think I've gotten this working as is, but I'd like to write a unittest that confirms this. However all my attempts at using MockMvc (via webAppContextSetup) fail to provide me with a means of asserting that I get an accepted header. When I for instance try the following:

@Test
public void pollPending() throws Exception {
    MvcResult result = mockMvc.perform(get("/poll/{uuid}", uuidPending)).andReturn();
    mockMvc.perform(asyncDispatch(result))
            .andExpect(status().isAccepted());
}

I get the following stacktrace:

java.lang.IllegalStateException: Async result for handler [public org.springframework.web.context.request.async.DeferredResult> nl.bioprodict.blast.api.PollController.poll(java.lang.String)] was not set during the specified timeToWait=25000 at org.springframework.util.Assert.state(Assert.java:392) at org.springframework.test.web.servlet.DefaultMvcResult.getAsyncResult(DefaultMvcResult.java:143) at org.springframework.test.web.servlet.DefaultMvcResult.getAsyncResult(DefaultMvcResult.java:120) at org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch(MockMvcRequestBuilders.java:235) at nl.bioprodict.blast.docs.PollControllerDocumentation.pollPending(PollControllerDocumentation.java:53) ...

The Spring framework tests related to this that I could find all use mocking it seems: https://github.com/spring-projects/spring-framework/blob/master/spring-web/src/test/java/org/springframework/web/context/request/async/WebAsyncManagerTimeoutTests.java

How can I test the correct handling of the DeferredResult timeoutResult?

回答1:

In my case, after going through spring source code and setting the timeout (10000 millisecond) and getting async result solved it for me, as;

 mvcResult.getRequest().getAsyncContext().setTimeout(10000);
 mvcResult.getAsyncResult();

My whole test code was;

MvcResult mvcResult = this.mockMvc.perform(
                                post("<SOME_RELATIVE_URL>")
                                .contentType(MediaType.APPLICATION_JSON)
                                .content(<JSON_DATA>))
                        ***.andExpect(request().asyncStarted())***
                            .andReturn();

***mvcResult.getRequest().getAsyncContext().setTimeout(10000);***
***mvcResult.getAsyncResult();***

this.mockMvc
    .perform(asyncDispatch(mvcResult))
    .andDo(print())
    .andExpect(status().isOk());

Hope it helps..



回答2:

I ran across this problem using Spring 4.3, and managed to find a way to trigger the timeout callback from within the unit test. After getting the MvcResult, and before calling asyncDispatch(), you can insert code such as the following:

MockAsyncContext ctx = (MockAsyncContext) mvcResult.getRequest().getAsyncContext();
for (AsyncListener listener : ctx.getListeners()) {
    listener.onTimeout(null);
}

One of the async listeners for the request will invoke the DeferredResult's timeout callback.

So your unit test would look like this:

@Test
public void pollPending() throws Exception {
    MvcResult result = mockMvc.perform(get("/poll/{uuid}", uuidPending)).andReturn();
    MockAsyncContext ctx = (MockAsyncContext) result.getRequest().getAsyncContext();
    for (AsyncListener listener : ctx.getListeners()) {
        listener.onTimeout(null);
    }
    mockMvc.perform(asyncDispatch(result))
            .andExpect(status().isAccepted());
}