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 61b65b2bb30..5c93c783e20 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 @@ -448,6 +448,8 @@ public final class MockMvcTester { * assertThat(mvc.get().uri("/greet")).hasStatusOk(); * assertThat(mvc.get().uri("/greet").exchange()).hasStatusOk(); * + *
For assertions on the original asynchronous request that might + * still be in progress, use {@link #asyncExchange()}. * @see #exchange(Duration) to customize the timeout for async requests */ public MvcTestResult exchange() { @@ -458,12 +460,23 @@ public final class MockMvcTester { * 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. + *
For assertions on the original asynchronous request that might + * still be in progress, use {@link #asyncExchange()}. * @see #exchange() */ public MvcTestResult exchange(Duration timeToWait) { return MockMvcTester.this.exchange(this, timeToWait); } + /** + * Execute the request and do not attempt to wait for the completion of + * an asynchronous request. Contrary to {@link #exchange()}, this returns + * the original result that might still be in progress. + */ + public MvcTestResult asyncExchange() { + return MockMvcTester.this.perform(this); + } + @Override public MvcTestResultAssert assertThat() { return new MvcTestResultAssert(exchange(), MockMvcTester.this.jsonMessageConverter); @@ -493,6 +506,8 @@ public final class MockMvcTester { * assertThat(mvc.get().uri("/greet")).hasStatusOk(); * assertThat(mvc.get().uri("/greet").exchange()).hasStatusOk(); * + *
For assertions on the original asynchronous request that might + * still be in progress, use {@link #asyncExchange()}. * @see #exchange(Duration) to customize the timeout for async requests */ public MvcTestResult exchange() { @@ -503,12 +518,23 @@ public final class MockMvcTester { * 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. + *
For assertions on the original asynchronous request that might + * still be in progress, use {@link #asyncExchange()}. * @see #exchange() */ public MvcTestResult exchange(Duration timeToWait) { return MockMvcTester.this.exchange(this, timeToWait); } + /** + * Execute the request and do not attempt to wait for the completion of + * an asynchronous request. Contrary to {@link #exchange()}, this returns + * the original result that might still be in progress. + */ + public MvcTestResult asyncExchange() { + return MockMvcTester.this.perform(this); + } + @Override public MvcTestResultAssert assertThat() { return new MvcTestResultAssert(exchange(), MockMvcTester.this.jsonMessageConverter); 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 aebfac1c743..90ceeaa1b8b 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 @@ -55,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.MockMultipartMvcRequestBuilder; import org.springframework.test.web.servlet.assertj.MockMvcTester.MockMvcRequestBuilder; import org.springframework.ui.Model; import org.springframework.validation.Errors; @@ -105,6 +106,9 @@ public class MockMvcTesterIntegrationTests { @Nested class PerformTests { + private final MockMultipartFile file = new MockMultipartFile("file", "content.txt", null, + "value".getBytes(StandardCharsets.UTF_8)); + @Test void syncRequestWithDefaultExchange() { assertThat(mvc.get().uri("/greet")).hasStatusOk(); @@ -116,6 +120,13 @@ public class MockMvcTesterIntegrationTests { .hasBodyTextEqualTo("name=Joe&someBoolean=true"); } + @Test + void asyncMultipartRequestWithDefaultExchange() { + assertThat(mvc.post().uri("/multipart-streaming").multipart() + .file(this.file).param("timeToWait", "100")) + .hasStatusOk().hasBodyTextEqualTo("name=Joe&file=content.txt"); + } + @Test void syncRequestWithExplicitExchange() { assertThat(mvc.get().uri("/greet").exchange()).hasStatusOk(); @@ -127,6 +138,13 @@ public class MockMvcTesterIntegrationTests { .hasStatusOk().hasBodyTextEqualTo("name=Joe&someBoolean=true"); } + @Test + void asyncMultipartRequestWitExplicitExchange() { + assertThat(mvc.post().uri("/multipart-streaming").multipart() + .file(this.file).param("timeToWait", "100").exchange()) + .hasStatusOk().hasBodyTextEqualTo("name=Joe&file=content.txt"); + } + @Test void syncRequestWithExplicitExchangeIgnoresDuration() { Duration timeToWait = mock(Duration.class); @@ -140,6 +158,13 @@ public class MockMvcTesterIntegrationTests { .hasStatusOk().hasBodyTextEqualTo("name=Joe&someBoolean=true"); } + @Test + void asyncMultipartRequestWithExplicitExchangeAndEnoughTimeToWait() { + assertThat(mvc.post().uri("/multipart-streaming").multipart() + .file(this.file).param("timeToWait", "100").exchange(Duration.ofMillis(200))) + .hasStatusOk().hasBodyTextEqualTo("name=Joe&file=content.txt"); + } + @Test void asyncRequestWithExplicitExchangeAndNotEnoughTimeToWait() { MockMvcRequestBuilder builder = mvc.get().uri("/streaming").param("timeToWait", "500"); @@ -147,6 +172,15 @@ public class MockMvcTesterIntegrationTests { .isThrownBy(() -> builder.exchange(Duration.ofMillis(100))) .withMessageContaining("was not set during the specified timeToWait=100"); } + + @Test + void asyncMultipartRequestWithExplicitExchangeAndNotEnoughTimeToWait() { + MockMultipartMvcRequestBuilder builder = mvc.post().uri("/multipart-streaming").multipart() + .file(this.file).param("timeToWait", "500"); + assertThatIllegalStateException() + .isThrownBy(() -> builder.exchange(Duration.ofMillis(100))) + .withMessageContaining("was not set during the specified timeToWait=100"); + } } @Nested @@ -154,14 +188,13 @@ public class MockMvcTesterIntegrationTests { @Test void hasAsyncStartedTrue() { - // Need #perform as the regular exchange waits for async completion automatically - assertThat(mvc.perform(mvc.get().uri("/callable").accept(MediaType.APPLICATION_JSON))) + assertThat(mvc.get().uri("/callable").accept(MediaType.APPLICATION_JSON).asyncExchange()) .request().hasAsyncStarted(true); } @Test void hasAsyncStartedFalse() { - assertThat(mvc.get().uri("/greet")).request().hasAsyncStarted(false); + assertThat(mvc.get().uri("/greet").asyncExchange()).request().hasAsyncStarted(false); } @Test @@ -325,8 +358,7 @@ public class MockMvcTesterIntegrationTests { @Test void asyncResult() { - // Need #perform as the regular exchange waits for async completion automatically - MvcTestResult result = mvc.perform(mvc.get().uri("/callable").accept(MediaType.APPLICATION_JSON)); + MvcTestResult result = mvc.get().uri("/callable").accept(MediaType.APPLICATION_JSON).asyncExchange(); assertThat(result.getMvcResult().getAsyncResult()) .asInstanceOf(InstanceOfAssertFactories.map(String.class, Object.class)) .containsOnly(entry("key", "value")); @@ -694,9 +726,24 @@ public class MockMvcTesterIntegrationTests { } @PutMapping("/multipart-put") - public ModelAndView multiPartViaHttpPut(@RequestParam MultipartFile file) { + ModelAndView multiPartViaHttpPut(@RequestParam MultipartFile file) { return new ModelAndView("index", Map.of("name", file.getName())); } + + @PostMapping("/multipart-streaming") + StreamingResponseBody streaming(@RequestParam MultipartFile file, @RequestParam long timeToWait) { + return out -> { + PrintStream stream = new PrintStream(out, true, StandardCharsets.UTF_8); + stream.print("name=Joe"); + try { + Thread.sleep(timeToWait); + stream.print("&file=" + file.getOriginalFilename()); + } + 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 80155a87734..cdc64fb5dcb 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 @@ -32,7 +32,6 @@ 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; @@ -265,9 +264,7 @@ class AsyncTests { void printAsyncResult() { StringWriter asyncWriter = new StringWriter(); - // Use #perform to not complete asynchronous request automatically - RequestBuilder requestBuilder = this.mockMvc.get().uri("/1").param("deferredResult", "true"); - MvcTestResult result = this.mockMvc.perform(requestBuilder); + MvcTestResult result = this.mockMvc.get().uri("/1").param("deferredResult", "true").asyncExchange(); assertThat(result).debug(asyncWriter).request().hasAsyncStarted(true); assertThat(asyncWriter.toString()).contains("Async started = true"); asyncWriter = new StringWriter(); // Reset