spring-projects / spring-framework

Spring Framework
https://spring.io/projects/spring-framework
Apache License 2.0
56.72k stars 38.15k forks source link

MockMvc - IllegalStateException: Async result for handler was not set during specified timeToWait=-1 [SPR-16869] #21408

Closed spring-projects-issues closed 6 years ago

spring-projects-issues commented 6 years ago

Adrian S opened SPR-16869 and commented

When trying to test Server-Sent Events endpoint created using rxjava2 and spring's ReactiveTypeHandler you encounter:

java.lang.IllegalStateException: Async result for handler [io.reactivex.Flowable<java.lang.String> com.example.asyncssebug.MockMvcAsyncBugTest$TestApp.sse()] was not set during the specified timeToWait=-1java.lang.IllegalStateException: Async result for handler [io.reactivex.Flowable<java.lang.String> com.example.asyncssebug.MockMvcAsyncBugTest$TestApp.sse()] was not set during the specified timeToWait=-1
 at org.springframework.test.web.servlet.DefaultMvcResult.getAsyncResult(DefaultMvcResult.java:145) at org.springframework.test.web.servlet.DefaultMvcResult.getAsyncResult(DefaultMvcResult.java:136) at org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch(MockMvcRequestBuilders.java:269)

Example test code to reproduce bug along with walkaround:

package com.example.asyncssebug;

import io.reactivex.Flowable;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.awt.*;
import java.util.concurrent.TimeUnit;

import static org.hamcrest.Matchers.nullValue;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = MockMvcAsyncBugTest.TestApp.class)
@AutoConfigureMockMvc
public class MockMvcAsyncBugTest {

    @RestController
    @RequestMapping("/events")
    @SpringBootApplication
    public static class TestApp {

        @GetMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)
        Flowable<String> sse() {
            return Flowable.intervalRange(0, 3, 0, 1, TimeUnit.SECONDS)
                    .map(aLong -> String.format("event%d", aLong));
        }
    }

    @Autowired
    MockMvc mockMvc;

    @Test
    public void failsWithIllegalStateExceptionAsyncResultForHandlerWasNotSetDuringSpecifiedTimeToWait() throws Exception {
        MvcResult mvcResult = mockMvc.perform(get("/events"))
                .andExpect(request().asyncStarted())
                .andExpect(status().isOk())
                .andReturn();

        mockMvc.perform(asyncDispatch(mvcResult))
                .andExpect(content().string("data:event0\n\ndata:event1\n\ndata:event2\n\n"));
    }

    @Test
    public void alsoFailsWithIllegalStateExceptionAsyncResultForHandlerWasNotSetDuringSpecifiedTimeToWait() throws Exception {
        mockMvc.perform(get("/events"))
                .andExpect(request().asyncStarted())
                .andExpect(request().asyncResult(nullValue()))
                .andExpect(status().isOk())
                .andExpect(content().string("data:event0\n\ndata:event1\n\ndata:event2\n\n"))
                .andReturn();
    }

    @Test
    public void walkaroundToMakeItWork() throws Exception {
        MvcResult mvcResult = mockMvc.perform(get("/events"))
                .andExpect(request().asyncStarted())
                .andExpect(status().isOk())
                .andReturn();
        mvcResult.getAsyncResult(5000L); // walkaround
        mockMvc.perform(asyncDispatch(mvcResult))
                .andExpect(content().string("data:event0\n\ndata:event1\n\ndata:event2\n\n"));
    }
}

Spring Boot version used is 2.0.2.RELEASE

Seems like default getAsyncResult(-1) instead of waiting forever doesn't wait at all. As a walkaround you can add mvcResult.getAsyncResult(5000L) and then perform asyncDispatch on the mvcResult

 


Affects: 5.0.6

Referenced from: commits https://github.com/spring-projects/spring-framework/commit/2a993bf9ff7c9a4fbb1edef8ea1e7f96ac0a1afc, https://github.com/spring-projects/spring-framework/commit/9d36fd0b68883847260863cec7131d4e77720522

spring-projects-issues commented 6 years ago

Rossen Stoyanchev commented

From the Javadoc of MvcResult#getAsyncResult:

* @param timeToWait how long to wait for the async result to be set, in
*   milliseconds; if -1, then the async request timeout value is used,
*  i.e.{@link org.springframework.mock.web.MockAsyncContext#getTimeout()}.

So -1 in this context actually means fallback on the timeout value of the async request. That's typically something like 10 seconds (e.g. on Tomcat), as well as in our MockAsyncContext, and it can be customized globally (via WebMvcConfigurer. However in the case of SSE the async request is explicitly set to -1, which in the Servlet API does mean wait forever, but then DefaultMvcResult needs to translate such a fallback value into a blocking call.

hantsy commented 3 years ago

I encountered the same issue when writing test against SSE endpoints using MockMvc, even added the workaround in the original post, it still failed.

java.lang.IllegalStateException: Async result for handler [com.example.demo.SseController#sseMessages()] was not set during the specified timeToWait=5000

    at org.springframework.test.web.servlet.DefaultMvcResult.getAsyncResult(DefaultMvcResult.java:146)
    at com.example.demo.SseControllerTests.testSseEndpoints(SseControllerTests.java:61)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:688)
    at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
    at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
    at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$6(TestMethodTestDescriptor.java:210)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:206)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:131)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:65)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:143)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:143)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:108)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:96)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:75)
    at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:71)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
    at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:235)
    at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)
rstoyanchev commented 3 years ago

MockMvc is not a good fit for testing streaming responses. There is a section on Streaming Responses in the docs for MockMvc. Note that WebTestClient can be set up as a test client against MockMvc.

hantsy commented 3 years ago

I have an example project written with WebMvc SseEmitter.

When switching to use MockMvcWebTestCient in tests, I still got the same exception.

org.springframework.web.reactive.function.client.WebClientRequestException: Async result for handler 
[com.example.demo.SseController#sseMessages()] was not set during the specified timeToWait=1000; nested exception is 
java.lang.IllegalStateException: Async result for handler [com.example.demo.SseController#sseMessages()] was not set during 
the specified timeToWait=1000

I have to add sseEmitter.complete in the controller to make the MockMvc and MockMvcWebTestClient work, else it will be blocked till it is timeout, and threw the exceptions.

rstoyanchev commented 3 years ago

The idea is you would use WebTestClient as the API for all tests but for SSE tests it would be against a live server, i.e. WebTestClient.bindToServer.