Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to test DeferredResult timeoutResult

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?

like image 477
Tim Avatar asked Dec 17 '15 19:12

Tim


People also ask

How does DeferredResult work?

DeferredResult provides an alternative to using a Callable for asynchronous request processing. While a Callable is executed concurrently on behalf of the application, with a DeferredResult the application can produce the result from a thread of its choice.

What is DeferredResult in spring boot?

DeferredResult, available from Spring 3.2 onwards, assists in offloading a long-running computation from an http-worker thread to a separate thread. Although the other thread will take some resources for computation, the worker threads are not blocked in the meantime and can handle incoming client requests.


2 Answers

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

like image 69
myuce Avatar answered Sep 19 '22 15:09

myuce


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());
}
like image 30
Kenster Avatar answered Sep 20 '22 15:09

Kenster