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 {
* assertThat(mvc.get().uri("/greet")).hasStatusOk(); * assertThat(mvc.get().uri("/greet")).hasStatusOk();
* assertThat(mvc.get().uri("/greet").exchange()).hasStatusOk(); * assertThat(mvc.get().uri("/greet").exchange()).hasStatusOk();
* </code></pre> * </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 * @see #exchange(Duration) to customize the timeout for async requests
*/ */
public MvcTestResult exchange() { public MvcTestResult exchange() {
@ -458,12 +460,23 @@ public final class MockMvcTester {
* Execute the request and wait at most the given {@code timeToWait} * Execute the request and wait at most the given {@code timeToWait}
* duration for the asynchronous request to complete. If the request * duration for the asynchronous request to complete. If the request
* is not asynchronous, the {@code timeToWait} is ignored. * 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() * @see #exchange()
*/ */
public MvcTestResult exchange(Duration timeToWait) { public MvcTestResult exchange(Duration timeToWait) {
return MockMvcTester.this.exchange(this, 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 @Override
public MvcTestResultAssert assertThat() { public MvcTestResultAssert assertThat() {
return new MvcTestResultAssert(exchange(), MockMvcTester.this.jsonMessageConverter); 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")).hasStatusOk();
* assertThat(mvc.get().uri("/greet").exchange()).hasStatusOk(); * assertThat(mvc.get().uri("/greet").exchange()).hasStatusOk();
* </code></pre> * </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 * @see #exchange(Duration) to customize the timeout for async requests
*/ */
public MvcTestResult exchange() { public MvcTestResult exchange() {
@ -503,12 +518,23 @@ public final class MockMvcTester {
* Execute the request and wait at most the given {@code timeToWait} * Execute the request and wait at most the given {@code timeToWait}
* duration for the asynchronous request to complete. If the request * duration for the asynchronous request to complete. If the request
* is not asynchronous, the {@code timeToWait} is ignored. * 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() * @see #exchange()
*/ */
public MvcTestResult exchange(Duration timeToWait) { public MvcTestResult exchange(Duration timeToWait) {
return MockMvcTester.this.exchange(this, 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 @Override
public MvcTestResultAssert assertThat() { public MvcTestResultAssert assertThat() {
return new MvcTestResultAssert(exchange(), MockMvcTester.this.jsonMessageConverter); 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;
import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig; import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig;
import org.springframework.test.web.Person; import org.springframework.test.web.Person;
import org.springframework.test.web.servlet.ResultMatcher; 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.test.web.servlet.assertj.MockMvcTester.MockMvcRequestBuilder;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.validation.Errors; import org.springframework.validation.Errors;
@ -105,6 +106,9 @@ public class MockMvcTesterIntegrationTests {
@Nested @Nested
class PerformTests { class PerformTests {
private final MockMultipartFile file = new MockMultipartFile("file", "content.txt", null,
"value".getBytes(StandardCharsets.UTF_8));
@Test @Test
void syncRequestWithDefaultExchange() { void syncRequestWithDefaultExchange() {
assertThat(mvc.get().uri("/greet")).hasStatusOk(); assertThat(mvc.get().uri("/greet")).hasStatusOk();
@ -116,6 +120,13 @@ public class MockMvcTesterIntegrationTests {
.hasBodyTextEqualTo("name=Joe&someBoolean=true"); .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 @Test
void syncRequestWithExplicitExchange() { void syncRequestWithExplicitExchange() {
assertThat(mvc.get().uri("/greet").exchange()).hasStatusOk(); assertThat(mvc.get().uri("/greet").exchange()).hasStatusOk();
@ -127,6 +138,13 @@ public class MockMvcTesterIntegrationTests {
.hasStatusOk().hasBodyTextEqualTo("name=Joe&someBoolean=true"); .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 @Test
void syncRequestWithExplicitExchangeIgnoresDuration() { void syncRequestWithExplicitExchangeIgnoresDuration() {
Duration timeToWait = mock(Duration.class); Duration timeToWait = mock(Duration.class);
@ -140,6 +158,13 @@ public class MockMvcTesterIntegrationTests {
.hasStatusOk().hasBodyTextEqualTo("name=Joe&someBoolean=true"); .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 @Test
void asyncRequestWithExplicitExchangeAndNotEnoughTimeToWait() { void asyncRequestWithExplicitExchangeAndNotEnoughTimeToWait() {
MockMvcRequestBuilder builder = mvc.get().uri("/streaming").param("timeToWait", "500"); MockMvcRequestBuilder builder = mvc.get().uri("/streaming").param("timeToWait", "500");
@ -147,6 +172,15 @@ public class MockMvcTesterIntegrationTests {
.isThrownBy(() -> builder.exchange(Duration.ofMillis(100))) .isThrownBy(() -> builder.exchange(Duration.ofMillis(100)))
.withMessageContaining("was not set during the specified timeToWait=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 @Nested
@ -154,14 +188,13 @@ public class MockMvcTesterIntegrationTests {
@Test @Test
void hasAsyncStartedTrue() { void hasAsyncStartedTrue() {
// Need #perform as the regular exchange waits for async completion automatically assertThat(mvc.get().uri("/callable").accept(MediaType.APPLICATION_JSON).asyncExchange())
assertThat(mvc.perform(mvc.get().uri("/callable").accept(MediaType.APPLICATION_JSON)))
.request().hasAsyncStarted(true); .request().hasAsyncStarted(true);
} }
@Test @Test
void hasAsyncStartedFalse() { void hasAsyncStartedFalse() {
assertThat(mvc.get().uri("/greet")).request().hasAsyncStarted(false); assertThat(mvc.get().uri("/greet").asyncExchange()).request().hasAsyncStarted(false);
} }
@Test @Test
@ -325,8 +358,7 @@ public class MockMvcTesterIntegrationTests {
@Test @Test
void asyncResult() { void asyncResult() {
// Need #perform as the regular exchange waits for async completion automatically MvcTestResult result = mvc.get().uri("/callable").accept(MediaType.APPLICATION_JSON).asyncExchange();
MvcTestResult result = mvc.perform(mvc.get().uri("/callable").accept(MediaType.APPLICATION_JSON));
assertThat(result.getMvcResult().getAsyncResult()) assertThat(result.getMvcResult().getAsyncResult())
.asInstanceOf(InstanceOfAssertFactories.map(String.class, Object.class)) .asInstanceOf(InstanceOfAssertFactories.map(String.class, Object.class))
.containsOnly(entry("key", "value")); .containsOnly(entry("key", "value"));
@ -694,9 +726,24 @@ public class MockMvcTesterIntegrationTests {
} }
@PutMapping("/multipart-put") @PutMapping("/multipart-put")
public ModelAndView multiPartViaHttpPut(@RequestParam MultipartFile file) { ModelAndView multiPartViaHttpPut(@RequestParam MultipartFile file) {
return new ModelAndView("index", Map.of("name", file.getName())); 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 @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;
import org.springframework.test.web.Person; import org.springframework.test.web.Person;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult; 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.MockMvcTester;
import org.springframework.test.web.servlet.assertj.MvcTestResult; import org.springframework.test.web.servlet.assertj.MvcTestResult;
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ExceptionHandler;
@ -265,9 +264,7 @@ class AsyncTests {
void printAsyncResult() { void printAsyncResult() {
StringWriter asyncWriter = new StringWriter(); StringWriter asyncWriter = new StringWriter();
// Use #perform to not complete asynchronous request automatically MvcTestResult result = this.mockMvc.get().uri("/1").param("deferredResult", "true").asyncExchange();
RequestBuilder requestBuilder = this.mockMvc.get().uri("/1").param("deferredResult", "true");
MvcTestResult result = this.mockMvc.perform(requestBuilder);
assertThat(result).debug(asyncWriter).request().hasAsyncStarted(true); assertThat(result).debug(asyncWriter).request().hasAsyncStarted(true);
assertThat(asyncWriter.toString()).contains("Async started = true"); assertThat(asyncWriter.toString()).contains("Async started = true");
asyncWriter = new StringWriter(); // Reset asyncWriter = new StringWriter(); // Reset

Loading…
Cancel
Save