Browse Source

Improve support of async request in MockMvcTester

This commit improves the handling of asynchronous requests by offering
a way to opt-in for the raw async result. This provides first class
support for asserting a request that might still be in process as well
as the asyncResult, if necessary.

See gh-33040
pull/33073/head
Stéphane Nicoll 2 years ago
parent
commit
24bbc6d80d
  1. 26
      spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MockMvcTester.java
  2. 59
      spring-test/src/test/java/org/springframework/test/web/servlet/assertj/MockMvcTesterIntegrationTests.java
  3. 5
      spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/AsyncTests.java

26
spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MockMvcTester.java

@ -448,6 +448,8 @@ public final class MockMvcTester { @@ -448,6 +448,8 @@ public final class MockMvcTester {
* assertThat(mvc.get().uri("/greet")).hasStatusOk();
* assertThat(mvc.get().uri("/greet").exchange()).hasStatusOk();
* </code></pre>
* <p>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 { @@ -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.
* <p>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 { @@ -493,6 +506,8 @@ public final class MockMvcTester {
* assertThat(mvc.get().uri("/greet")).hasStatusOk();
* assertThat(mvc.get().uri("/greet").exchange()).hasStatusOk();
* </code></pre>
* <p>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 { @@ -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.
* <p>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);

59
spring-test/src/test/java/org/springframework/test/web/servlet/assertj/MockMvcTesterIntegrationTests.java

@ -55,6 +55,7 @@ import org.springframework.stereotype.Controller; @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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

5
spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/AsyncTests.java

@ -32,7 +32,6 @@ import org.springframework.http.ResponseEntity; @@ -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 { @@ -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

Loading…
Cancel
Save