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:
Stéphane Nicoll
2024-06-17 07:55:53 +02:00
parent a1b0099795
commit a7503e7200
5 changed files with 359 additions and 134 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -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