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
This commit is contained in:
@@ -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}.
|
||||
* <p>For simple assertions, you can wrap this builder in
|
||||
* {@code assertThat} rather than calling this method explicitly:
|
||||
* <pre><code class="java">
|
||||
* // These two examples are equivalent
|
||||
* assertThat(mvc.get().uri("/greet")).hasStatusOk();
|
||||
* assertThat(mvc.get().uri("/greet").exchange()).hasStatusOk();
|
||||
* </code></pre>
|
||||
* @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}.
|
||||
* <p>For simple assertions, you can wrap this builder in
|
||||
* {@code assertThat} rather than calling this method explicitly:
|
||||
* <pre><code class="java">
|
||||
* // These two examples are equivalent
|
||||
* assertThat(mvc.get().uri("/greet")).hasStatusOk();
|
||||
* assertThat(mvc.get().uri("/greet").exchange()).hasStatusOk();
|
||||
* </code></pre>
|
||||
* @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
|
||||
|
||||
@@ -38,6 +38,10 @@ import org.springframework.test.web.servlet.MvcResult;
|
||||
* {@linkplain #getMvcResult() result} will fail with an exception.</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>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<MvcTestResultAssert> {
|
||||
return getMvcResult().getResponse();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the exception that was thrown unexpectedly while processing the
|
||||
* request, if any.
|
||||
|
||||
@@ -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<M
|
||||
return new MapAssert<>(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<Object> asyncResult() {
|
||||
request().hasAsyncStarted(true);
|
||||
return Assertions.assertThat(getMvcResult().getAsyncResult()).as("Async result");
|
||||
}
|
||||
|
||||
/**
|
||||
* Print {@link MvcResult} details to {@code System.out}.
|
||||
* <p>You must call it <b>before</b> calling the assertion otherwise it is ignored
|
||||
|
||||
Reference in New Issue
Block a user