From a7503e720052a1028f31a9e0774fc54b2c2f4920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 17 Jun 2024 07:55:53 +0200 Subject: [PATCH] Add explicit support for asynchronous requests in MockMvcTester This commit makes asynchronous requests first class by providing a MvcTestResult that represents the final, completed, state of a request by default. Previously, the result was an intermediate step that may require an ASYNC dispatch to be fully usable. Now it is de facto immutable. To make things a bit more explicit, an `.exchange(Duration)` method has been added to provide a dedicated time to wait. The default applies a default timeout that is consistent with what MVcResult#getAsyncResult does. Given that we apply the ASYNC dispatch automatically, the intermediate response is no longer available by default. As a result, the asyncResult is not available for assertions. As always, it is possible to use plain MockMvc by using the `perform` method that takes the regular RequestBuilder as an input. When this method is invoked, no asynchronous handling is done. Closes gh-33040 --- .../web/servlet/assertj/MockMvcTester.java | 80 ++++- .../web/servlet/assertj/MvcTestResult.java | 5 +- .../servlet/assertj/MvcTestResultAssert.java | 12 - .../MockMvcTesterIntegrationTests.java | 84 ++++- .../samples/standalone/AsyncTests.java | 312 ++++++++++++------ 5 files changed, 359 insertions(+), 134 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MockMvcTester.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MockMvcTester.java index a1ce85d696..61b65b2bb3 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MockMvcTester.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MockMvcTester.java @@ -17,12 +17,14 @@ package org.springframework.test.web.servlet.assertj; import java.net.URI; +import java.time.Duration; import java.util.Arrays; import java.util.Collection; import java.util.Map; import java.util.function.Function; import java.util.stream.StreamSupport; +import jakarta.servlet.DispatcherType; import org.assertj.core.api.AssertProvider; import org.springframework.http.HttpMethod; @@ -384,6 +386,34 @@ public final class MockMvcTester { .findFirst().orElse(null); } + /** + * Execute the request using the specified {@link RequestBuilder}. If the + * request is processing asynchronously, wait at most the given + * {@code timeToWait} duration. If not specified, then fall back on the + * timeout value associated with the async request, see + * {@link org.springframework.mock.web.MockAsyncContext#setTimeout}. + */ + MvcTestResult exchange(RequestBuilder requestBuilder, @Nullable Duration timeToWait) { + MvcTestResult result = perform(requestBuilder); + if (result.getUnresolvedException() == null) { + if (result.getRequest().isAsyncStarted()) { + // Wait for async result before dispatching + long waitMs = (timeToWait != null ? timeToWait.toMillis() : -1); + result.getMvcResult().getAsyncResult(waitMs); + + // Perform ASYNC dispatch + RequestBuilder dispatchRequest = servletContext -> { + MockHttpServletRequest request = result.getMvcResult().getRequest(); + request.setDispatcherType(DispatcherType.ASYNC); + request.setAsyncStarted(false); + return request; + }; + return perform(dispatchRequest); + } + } + return result; + } + /** * A builder for {@link MockHttpServletRequest} that supports AssertJ. @@ -407,8 +437,31 @@ public final class MockMvcTester { return new MockMultipartMvcRequestBuilder(this); } + /** + * Execute the request. If the request is processing asynchronously, + * wait at most the given timeout value associated with the async request, + * see {@link org.springframework.mock.web.MockAsyncContext#setTimeout}. + *

For simple assertions, you can wrap this builder in + * {@code assertThat} rather than calling this method explicitly: + *


+		 * // These two examples are equivalent
+		 * assertThat(mvc.get().uri("/greet")).hasStatusOk();
+		 * assertThat(mvc.get().uri("/greet").exchange()).hasStatusOk();
+		 * 
+ * @see #exchange(Duration) to customize the timeout for async requests + */ public MvcTestResult exchange() { - return perform(this); + return MockMvcTester.this.exchange(this, null); + } + + /** + * Execute the request and wait at most the given {@code timeToWait} + * duration for the asynchronous request to complete. If the request + * is not asynchronous, the {@code timeToWait} is ignored. + * @see #exchange() + */ + public MvcTestResult exchange(Duration timeToWait) { + return MockMvcTester.this.exchange(this, timeToWait); } @Override @@ -429,8 +482,31 @@ public final class MockMvcTester { merge(currentBuilder); } + /** + * Execute the request. If the request is processing asynchronously, + * wait at most the given timeout value associated with the async request, + * see {@link org.springframework.mock.web.MockAsyncContext#setTimeout}. + *

For simple assertions, you can wrap this builder in + * {@code assertThat} rather than calling this method explicitly: + *


+		 * // These two examples are equivalent
+		 * assertThat(mvc.get().uri("/greet")).hasStatusOk();
+		 * assertThat(mvc.get().uri("/greet").exchange()).hasStatusOk();
+		 * 
+ * @see #exchange(Duration) to customize the timeout for async requests + */ public MvcTestResult exchange() { - return perform(this); + return MockMvcTester.this.exchange(this, null); + } + + /** + * Execute the request and wait at most the given {@code timeToWait} + * duration for the asynchronous request to complete. If the request + * is not asynchronous, the {@code timeToWait} is ignored. + * @see #exchange() + */ + public MvcTestResult exchange(Duration timeToWait) { + return MockMvcTester.this.exchange(this, timeToWait); } @Override diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcTestResult.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcTestResult.java index 805953567e..20b6e942db 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcTestResult.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcTestResult.java @@ -38,6 +38,10 @@ import org.springframework.test.web.servlet.MvcResult; * {@linkplain #getMvcResult() result} will fail with an exception. * * + *

If the request was asynchronous, it is fully resolved at this point and + * regular assertions can be applied without having to wait for the completion + * of the response. + * * @author Stephane Nicoll * @author Brian Clozel * @since 6.2 @@ -69,7 +73,6 @@ public interface MvcTestResult extends AssertProvider { return getMvcResult().getResponse(); } - /** * Return the exception that was thrown unexpectedly while processing the * request, if any. diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcTestResultAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcTestResultAssert.java index 614e32da2f..53ccf51367 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcTestResultAssert.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcTestResultAssert.java @@ -28,7 +28,6 @@ import org.assertj.core.api.AbstractStringAssert; import org.assertj.core.api.AbstractThrowableAssert; import org.assertj.core.api.Assertions; import org.assertj.core.api.MapAssert; -import org.assertj.core.api.ObjectAssert; import org.assertj.core.error.BasicErrorMessageFactory; import org.assertj.core.internal.Failures; @@ -128,17 +127,6 @@ public class MvcTestResultAssert extends AbstractMockHttpServletResponseAssert(getMvcResult().getFlashMap()); } - /** - * Verify that {@linkplain AbstractHttpServletRequestAssert#hasAsyncStarted(boolean) - * asynchronous processing has started} and return a new - * {@linkplain ObjectAssert assertion} object that uses the asynchronous - * result as the object to test. - */ - public ObjectAssert asyncResult() { - request().hasAsyncStarted(true); - return Assertions.assertThat(getMvcResult().getAsyncResult()).as("Async result"); - } - /** * Print {@link MvcResult} details to {@code System.out}. *

You must call it before calling the assertion otherwise it is ignored diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/MockMvcTesterIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/MockMvcTesterIntegrationTests.java index e55d2a0a21..aebfac1c74 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/MockMvcTesterIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/MockMvcTesterIntegrationTests.java @@ -20,6 +20,7 @@ import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.io.StringWriter; import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -36,6 +37,7 @@ import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.Part; import jakarta.validation.Valid; import jakarta.validation.constraints.Size; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; @@ -53,6 +55,7 @@ import org.springframework.stereotype.Controller; import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig; import org.springframework.test.web.Person; import org.springframework.test.web.servlet.ResultMatcher; +import org.springframework.test.web.servlet.assertj.MockMvcTester.MockMvcRequestBuilder; import org.springframework.ui.Model; import org.springframework.validation.Errors; import org.springframework.web.bind.annotation.GetMapping; @@ -73,14 +76,16 @@ import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import org.springframework.web.servlet.mvc.support.RedirectAttributes; import static java.util.Map.entry; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.InstanceOfAssertFactories.map; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; /** * Integration tests for {@link MockMvcTester}. @@ -97,12 +102,60 @@ public class MockMvcTesterIntegrationTests { this.mvc = MockMvcTester.from(wac); } + @Nested + class PerformTests { + + @Test + void syncRequestWithDefaultExchange() { + assertThat(mvc.get().uri("/greet")).hasStatusOk(); + } + + @Test + void asyncRequestWithDefaultExchange() { + assertThat(mvc.get().uri("/streaming").param("timeToWait", "100")).hasStatusOk() + .hasBodyTextEqualTo("name=Joe&someBoolean=true"); + } + + @Test + void syncRequestWithExplicitExchange() { + assertThat(mvc.get().uri("/greet").exchange()).hasStatusOk(); + } + + @Test + void asyncRequestWithExplicitExchange() { + assertThat(mvc.get().uri("/streaming").param("timeToWait", "100").exchange()) + .hasStatusOk().hasBodyTextEqualTo("name=Joe&someBoolean=true"); + } + + @Test + void syncRequestWithExplicitExchangeIgnoresDuration() { + Duration timeToWait = mock(Duration.class); + assertThat(mvc.get().uri("/greet").exchange(timeToWait)).hasStatusOk(); + verifyNoInteractions(timeToWait); + } + + @Test + void asyncRequestWithExplicitExchangeAndEnoughTimeToWait() { + assertThat(mvc.get().uri("/streaming").param("timeToWait", "100").exchange(Duration.ofMillis(200))) + .hasStatusOk().hasBodyTextEqualTo("name=Joe&someBoolean=true"); + } + + @Test + void asyncRequestWithExplicitExchangeAndNotEnoughTimeToWait() { + MockMvcRequestBuilder builder = mvc.get().uri("/streaming").param("timeToWait", "500"); + assertThatIllegalStateException() + .isThrownBy(() -> builder.exchange(Duration.ofMillis(100))) + .withMessageContaining("was not set during the specified timeToWait=100"); + } + } + @Nested class RequestTests { @Test void hasAsyncStartedTrue() { - assertThat(mvc.get().uri("/callable").accept(MediaType.APPLICATION_JSON)) + // Need #perform as the regular exchange waits for async completion automatically + assertThat(mvc.perform(mvc.get().uri("/callable").accept(MediaType.APPLICATION_JSON))) .request().hasAsyncStarted(true); } @@ -272,8 +325,10 @@ public class MockMvcTesterIntegrationTests { @Test void asyncResult() { - assertThat(mvc.get().uri("/callable").accept(MediaType.APPLICATION_JSON)) - .asyncResult().asInstanceOf(map(String.class, Object.class)) + // Need #perform as the regular exchange waits for async completion automatically + MvcTestResult result = mvc.perform(mvc.get().uri("/callable").accept(MediaType.APPLICATION_JSON)); + assertThat(result.getMvcResult().getAsyncResult()) + .asInstanceOf(InstanceOfAssertFactories.map(String.class, Object.class)) .containsOnly(entry("key", "value")); } @@ -441,12 +496,6 @@ public class MockMvcTesterIntegrationTests { result -> assertThat(result).apply(mvcResult -> {})); } - @Test - void assertAsyncResultWithUnresolvedException() { - testAssertionFailureWithUnresolvableException( - result -> assertThat(result).asyncResult()); - } - @Test void assertContentTypeWithUnresolvedException() { testAssertionFailureWithUnresolvableException( @@ -617,6 +666,21 @@ public class MockMvcTesterIntegrationTests { public Callable> getCallable() { return () -> Collections.singletonMap("key", "value"); } + + @GetMapping("/streaming") + StreamingResponseBody streaming(@RequestParam long timeToWait) { + return out -> { + PrintStream stream = new PrintStream(out, true, StandardCharsets.UTF_8); + stream.print("name=Joe"); + try { + Thread.sleep(timeToWait); + stream.print("&someBoolean=true"); + } + catch (InterruptedException e) { + /* no-op */ + } + }; + } } @Controller diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/AsyncTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/AsyncTests.java index ec82a970b2..80155a8773 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/AsyncTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/AsyncTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import java.time.Duration; import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; @@ -31,6 +32,9 @@ import org.springframework.http.ResponseEntity; import org.springframework.test.web.Person; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.RequestBuilder; +import org.springframework.test.web.servlet.assertj.MockMvcTester; +import org.springframework.test.web.servlet.assertj.MvcTestResult; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseStatus; @@ -58,132 +62,222 @@ import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standal */ class AsyncTests { - private final AsyncController asyncController = new AsyncController(); + @Nested + class MockMvcTests { - private final MockMvc mockMvc = standaloneSetup(this.asyncController).build(); + private final MockMvc mockMvc = standaloneSetup(new AsyncController()).build(); + @Test + void callable() throws Exception { + MvcResult mvcResult = this.mockMvc.perform(get("/1").param("callable", "true")) + .andExpect(request().asyncStarted()) + .andExpect(request().asyncResult(equalTo(new Person("Joe")))) + .andExpect(request().asyncResult(new Person("Joe"))) + .andReturn(); - @Test - void callable() throws Exception { - MvcResult mvcResult = this.mockMvc.perform(get("/1").param("callable", "true")) - .andExpect(request().asyncStarted()) - .andExpect(request().asyncResult(equalTo(new Person("Joe")))) - .andExpect(request().asyncResult(new Person("Joe"))) - .andReturn(); + this.mockMvc.perform(asyncDispatch(mvcResult)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().string("{\"name\":\"Joe\",\"someDouble\":0.0,\"someBoolean\":false}")); + } - this.mockMvc.perform(asyncDispatch(mvcResult)) - .andExpect(status().isOk()) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(content().string("{\"name\":\"Joe\",\"someDouble\":0.0,\"someBoolean\":false}")); + @Test + void streaming() throws Exception { + this.mockMvc.perform(get("/1").param("streaming", "true")) + .andExpect(request().asyncStarted()) + .andDo(MvcResult::getAsyncResult) // fetch async result similar to "asyncDispatch" builder + .andExpect(status().isOk()) + .andExpect(content().string("name=Joe")); + } + + @Test + void streamingSlow() throws Exception { + this.mockMvc.perform(get("/1").param("streamingSlow", "true")) + .andExpect(request().asyncStarted()) + .andDo(MvcResult::getAsyncResult) + .andExpect(status().isOk()) + .andExpect(content().string("name=Joe&someBoolean=true")); + } + + @Test + void streamingJson() throws Exception { + this.mockMvc.perform(get("/1").param("streamingJson", "true")) + .andExpect(request().asyncStarted()) + .andDo(MvcResult::getAsyncResult) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().string("{\"name\":\"Joe\",\"someDouble\":0.5}")); + } + + @Test + void deferredResult() throws Exception { + MvcResult mvcResult = this.mockMvc.perform(get("/1").param("deferredResult", "true")) + .andExpect(request().asyncStarted()) + .andReturn(); + + this.mockMvc.perform(asyncDispatch(mvcResult)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().string("{\"name\":\"Joe\",\"someDouble\":0.0,\"someBoolean\":false}")); + } + + @Test + void deferredResultWithImmediateValue() throws Exception { + MvcResult mvcResult = this.mockMvc.perform(get("/1").param("deferredResultWithImmediateValue", "true")) + .andExpect(request().asyncStarted()) + .andExpect(request().asyncResult(new Person("Joe"))) + .andReturn(); + + this.mockMvc.perform(asyncDispatch(mvcResult)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().string("{\"name\":\"Joe\",\"someDouble\":0.0,\"someBoolean\":false}")); + } + + @Test // SPR-13079 + void deferredResultWithDelayedError() throws Exception { + MvcResult mvcResult = this.mockMvc.perform(get("/1").param("deferredResultWithDelayedError", "true")) + .andExpect(request().asyncStarted()) + .andReturn(); + + this.mockMvc.perform(asyncDispatch(mvcResult)) + .andExpect(status().is5xxServerError()) + .andExpect(content().string("Delayed Error")); + } + + @Test + void listenableFuture() throws Exception { + MvcResult mvcResult = this.mockMvc.perform(get("/1").param("listenableFuture", "true")) + .andExpect(request().asyncStarted()) + .andReturn(); + + this.mockMvc.perform(asyncDispatch(mvcResult)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().string("{\"name\":\"Joe\",\"someDouble\":0.0,\"someBoolean\":false}")); + } + + @Test // SPR-12597 + void completableFutureWithImmediateValue() throws Exception { + MvcResult mvcResult = this.mockMvc.perform(get("/1").param("completableFutureWithImmediateValue", "true")) + .andExpect(request().asyncStarted()) + .andReturn(); + + this.mockMvc.perform(asyncDispatch(mvcResult)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().string("{\"name\":\"Joe\",\"someDouble\":0.0,\"someBoolean\":false}")); + } + + @Test // SPR-12735 + void printAsyncResult() throws Exception { + StringWriter writer = new StringWriter(); + + MvcResult mvcResult = this.mockMvc.perform(get("/1").param("deferredResult", "true")) + .andDo(print(writer)) + .andExpect(request().asyncStarted()) + .andReturn(); + + assertThat(writer.toString()).contains("Async started = true"); + writer = new StringWriter(); + + this.mockMvc.perform(asyncDispatch(mvcResult)) + .andDo(print(writer)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().string("{\"name\":\"Joe\",\"someDouble\":0.0,\"someBoolean\":false}")); + + assertThat(writer.toString()).contains("Async started = false"); + } } - @Test - void streaming() throws Exception { - this.mockMvc.perform(get("/1").param("streaming", "true")) - .andExpect(request().asyncStarted()) - .andDo(MvcResult::getAsyncResult) // fetch async result similar to "asyncDispatch" builder - .andExpect(status().isOk()) - .andExpect(content().string("name=Joe")); - } + @Nested + class MockMvcTesterTests { - @Test - void streamingSlow() throws Exception { - this.mockMvc.perform(get("/1").param("streamingSlow", "true")) - .andExpect(request().asyncStarted()) - .andDo(MvcResult::getAsyncResult) - .andExpect(status().isOk()) - .andExpect(content().string("name=Joe&someBoolean=true")); - } + private final MockMvcTester mockMvc = MockMvcTester.of(new AsyncController()); - @Test - void streamingJson() throws Exception { - this.mockMvc.perform(get("/1").param("streamingJson", "true")) - .andExpect(request().asyncStarted()) - .andDo(MvcResult::getAsyncResult) - .andExpect(status().isOk()) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(content().string("{\"name\":\"Joe\",\"someDouble\":0.5}")); - } + @Test + void callable() { + assertThat(mockMvc.get().uri("/1").param("callable", "true")) + .hasStatusOk() + .hasContentTypeCompatibleWith(MediaType.APPLICATION_JSON) + .hasBodyTextEqualTo("{\"name\":\"Joe\",\"someDouble\":0.0,\"someBoolean\":false}"); + } - @Test - void deferredResult() throws Exception { - MvcResult mvcResult = this.mockMvc.perform(get("/1").param("deferredResult", "true")) - .andExpect(request().asyncStarted()) - .andReturn(); + @Test + void streaming() { + assertThat(this.mockMvc.get().uri("/1").param("streaming", "true")) + .hasStatusOk().hasBodyTextEqualTo("name=Joe"); + } - this.mockMvc.perform(asyncDispatch(mvcResult)) - .andExpect(status().isOk()) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(content().string("{\"name\":\"Joe\",\"someDouble\":0.0,\"someBoolean\":false}")); - } + @Test + void streamingSlow() { + assertThat(this.mockMvc.get().uri("/1").param("streamingSlow", "true")) + .hasStatusOk().hasBodyTextEqualTo("name=Joe&someBoolean=true"); + } - @Test - void deferredResultWithImmediateValue() throws Exception { - MvcResult mvcResult = this.mockMvc.perform(get("/1").param("deferredResultWithImmediateValue", "true")) - .andExpect(request().asyncStarted()) - .andExpect(request().asyncResult(new Person("Joe"))) - .andReturn(); + @Test + void streamingJson() { + assertThat(this.mockMvc.get().uri("/1").param("streamingJson", "true")) + .hasStatusOk() + .hasContentTypeCompatibleWith(MediaType.APPLICATION_JSON) + .hasBodyTextEqualTo("{\"name\":\"Joe\",\"someDouble\":0.5}"); + } - this.mockMvc.perform(asyncDispatch(mvcResult)) - .andExpect(status().isOk()) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(content().string("{\"name\":\"Joe\",\"someDouble\":0.0,\"someBoolean\":false}")); - } + @Test + void deferredResult() { + assertThat(this.mockMvc.get().uri("/1").param("deferredResult", "true")) + .hasStatusOk() + .hasContentTypeCompatibleWith(MediaType.APPLICATION_JSON) + .hasBodyTextEqualTo("{\"name\":\"Joe\",\"someDouble\":0.0,\"someBoolean\":false}"); + } - @Test // SPR-13079 - void deferredResultWithDelayedError() throws Exception { - MvcResult mvcResult = this.mockMvc.perform(get("/1").param("deferredResultWithDelayedError", "true")) - .andExpect(request().asyncStarted()) - .andReturn(); + @Test + void deferredResultWithImmediateValue() { + assertThat(this.mockMvc.get().uri("/1").param("deferredResultWithImmediateValue", "true")) + .hasStatusOk() + .hasContentTypeCompatibleWith(MediaType.APPLICATION_JSON) + .hasBodyTextEqualTo("{\"name\":\"Joe\",\"someDouble\":0.0,\"someBoolean\":false}"); + } - this.mockMvc.perform(asyncDispatch(mvcResult)) - .andExpect(status().is5xxServerError()) - .andExpect(content().string("Delayed Error")); - } + @Test // SPR-13079 + void deferredResultWithDelayedError() { + assertThat(this.mockMvc.get().uri("/1").param("deferredResultWithDelayedError", "true")) + .hasStatus5xxServerError().hasBodyTextEqualTo("Delayed Error"); + } - @Test - void listenableFuture() throws Exception { - MvcResult mvcResult = this.mockMvc.perform(get("/1").param("listenableFuture", "true")) - .andExpect(request().asyncStarted()) - .andReturn(); + @Test + void listenableFuture() { + assertThat(this.mockMvc.get().uri("/1").param("listenableFuture", "true")) + .hasStatusOk().hasContentTypeCompatibleWith(MediaType.APPLICATION_JSON) + .hasBodyTextEqualTo("{\"name\":\"Joe\",\"someDouble\":0.0,\"someBoolean\":false}"); + } - this.mockMvc.perform(asyncDispatch(mvcResult)) - .andExpect(status().isOk()) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(content().string("{\"name\":\"Joe\",\"someDouble\":0.0,\"someBoolean\":false}")); - } + @Test // SPR-12597 + void completableFutureWithImmediateValue() { + assertThat(this.mockMvc.get().uri("/1").param("completableFutureWithImmediateValue", "true")) + .hasStatusOk() + .hasContentTypeCompatibleWith(MediaType.APPLICATION_JSON) + .hasBodyTextEqualTo("{\"name\":\"Joe\",\"someDouble\":0.0,\"someBoolean\":false}"); + } - @Test // SPR-12597 - void completableFutureWithImmediateValue() throws Exception { - MvcResult mvcResult = this.mockMvc.perform(get("/1").param("completableFutureWithImmediateValue", "true")) - .andExpect(request().asyncStarted()) - .andReturn(); + @Test // SPR-12735 + void printAsyncResult() { + StringWriter asyncWriter = new StringWriter(); - this.mockMvc.perform(asyncDispatch(mvcResult)) - .andExpect(status().isOk()) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(content().string("{\"name\":\"Joe\",\"someDouble\":0.0,\"someBoolean\":false}")); - } - - @Test // SPR-12735 - void printAsyncResult() throws Exception { - StringWriter writer = new StringWriter(); - - MvcResult mvcResult = this.mockMvc.perform(get("/1").param("deferredResult", "true")) - .andDo(print(writer)) - .andExpect(request().asyncStarted()) - .andReturn(); - - assertThat(writer.toString()).contains("Async started = true"); - writer = new StringWriter(); - - this.mockMvc.perform(asyncDispatch(mvcResult)) - .andDo(print(writer)) - .andExpect(status().isOk()) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(content().string("{\"name\":\"Joe\",\"someDouble\":0.0,\"someBoolean\":false}")); - - assertThat(writer.toString()).contains("Async started = false"); + // Use #perform to not complete asynchronous request automatically + RequestBuilder requestBuilder = this.mockMvc.get().uri("/1").param("deferredResult", "true"); + MvcTestResult result = this.mockMvc.perform(requestBuilder); + assertThat(result).debug(asyncWriter).request().hasAsyncStarted(true); + assertThat(asyncWriter.toString()).contains("Async started = true"); + asyncWriter = new StringWriter(); // Reset + assertThat(this.mockMvc.perform(asyncDispatch(result.getMvcResult()))) + .debug(asyncWriter) + .hasStatusOk() + .hasContentTypeCompatibleWith(MediaType.APPLICATION_JSON) + .hasBodyTextEqualTo("{\"name\":\"Joe\",\"someDouble\":0.0,\"someBoolean\":false}"); + assertThat(asyncWriter.toString()).contains("Async started = false"); + } }