Provide better name for entry points

This commit renames AssertableMockMvc to MockMvcTester as the former
was too mouthful, and we are looking for a naming pattern that can be
applied consistently to test utilities that rely on AssertJ.

Rather than AssertableMvcResult, we now use MvcTestResult and it no
longer extends from MvcResult itself. That avoids having existing code
that is migrated to the new API whilst assigning the result to MvcResult
and get a bad DevXP as that doesn't bring any of the AssertJ support.

See gh-32712
This commit is contained in:
Stéphane Nicoll
2024-05-06 17:21:30 +02:00
parent 5567d14700
commit c8967de065
7 changed files with 155 additions and 192 deletions

View File

@@ -21,20 +21,17 @@ import org.springframework.lang.Nullable;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.web.servlet.FlashMap;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
/**
* The default {@link AssertableMvcResult} implementation.
* The default {@link MvcTestResult} implementation.
*
* @author Stephane Nicoll
* @since 6.2
*/
final class DefaultAssertableMvcResult implements AssertableMvcResult {
final class DefaultMvcTestResult implements MvcTestResult {
@Nullable
private final MvcResult target;
private final MvcResult mvcResult;
@Nullable
private final Exception unresolvedException;
@@ -42,86 +39,46 @@ final class DefaultAssertableMvcResult implements AssertableMvcResult {
@Nullable
private final GenericHttpMessageConverter<Object> jsonMessageConverter;
DefaultAssertableMvcResult(@Nullable MvcResult target, @Nullable Exception unresolvedException, @Nullable GenericHttpMessageConverter<Object> jsonMessageConverter) {
this.target = target;
DefaultMvcTestResult(@Nullable MvcResult mvcResult, @Nullable Exception unresolvedException, @Nullable GenericHttpMessageConverter<Object> jsonMessageConverter) {
this.mvcResult = mvcResult;
this.unresolvedException = unresolvedException;
this.jsonMessageConverter = jsonMessageConverter;
}
/**
* Return the exception that was thrown unexpectedly while processing the
* request, if any.
*/
public MvcResult getMvcResult() {
if (this.mvcResult == null) {
throw new IllegalStateException(
"Request has failed with unresolved exception " + this.unresolvedException);
}
return this.mvcResult;
}
@Nullable
public Exception getUnresolvedException() {
return this.unresolvedException;
}
@Override
public MockHttpServletRequest getRequest() {
return getTarget().getRequest();
return getMvcResult().getRequest();
}
@Override
public MockHttpServletResponse getResponse() {
return getTarget().getResponse();
return getMvcResult().getResponse();
}
@Override
@Nullable
public Object getHandler() {
return getTarget().getHandler();
}
@Override
@Nullable
public HandlerInterceptor[] getInterceptors() {
return getTarget().getInterceptors();
}
@Override
@Nullable
public ModelAndView getModelAndView() {
return getTarget().getModelAndView();
}
@Override
@Nullable
public Exception getResolvedException() {
return getTarget().getResolvedException();
return getMvcResult().getResolvedException();
}
@Override
public FlashMap getFlashMap() {
return getTarget().getFlashMap();
}
@Override
public Object getAsyncResult() {
return getTarget().getAsyncResult();
}
@Override
public Object getAsyncResult(long timeToWait) {
return getTarget().getAsyncResult(timeToWait);
}
private MvcResult getTarget() {
if (this.target == null) {
throw new IllegalStateException(
"Request has failed with unresolved exception " + this.unresolvedException);
}
return this.target;
}
/**
* Use AssertJ's {@link org.assertj.core.api.Assertions#assertThat assertThat}
* instead.
*/
@Override
public MvcResultAssert assertThat() {
return new MvcResultAssert(this, this.jsonMessageConverter);
public MvcTestResultAssert assertThat() {
return new MvcTestResultAssert(this, this.jsonMessageConverter);
}
}

View File

@@ -38,15 +38,46 @@ import org.springframework.util.Assert;
import org.springframework.web.context.WebApplicationContext;
/**
* {@link MockMvc} variant that tests Spring MVC exchanges and provides fluent
* assertions using {@link org.assertj.core.api.Assertions AssertJ}.
* Test Spring MVC applications with {@link MockMvc} for server request handling
* using {@link org.assertj.core.api.Assertions AssertJ}.
*
* <p>A tester instance can be created from a {@link WebApplicationContext}:
* <pre><code class='java'>
* // Create an instance with default settings
* MockMvcTester mvc = MockMvcTester.from(applicationContext);
*
* // Create an instance with a custom Filter
* MockMvcTester mvc = MockMvcTester.from(applicationContext,
* builder -> builder.addFilters(filter).build());
* </code></pre>
*
* <p>A tester can be created standalone by providing the controller(s) to
* include in a standalone setup:<pre><code class='java'>
* // Create an instance for PersonController
* MockMvcTester mvc = MockMvcTester.of(new PersonController());
* </code></pre>
*
* <p>Once a test instance is available, you can perform requests in
* a similar fashion as with {@link MockMvc}, and wrapping the result in
* {@code assertThat} provides access to assertions. For instance:
* <pre><code class='java'>
* // perform a GET on /hi and assert the response body is equal to Hello
* assertThat(mvc.perform(get("/hi")))
* .hasStatusOk().hasBodyTextEqualTo("Hello");
* </code></pre>
*
* <p>A main difference with {@link MockMvc} is that an unresolved exception
* is not thrown directly. Rather an {@link AssertableMvcResult} is available
* with an {@link AssertableMvcResult#getUnresolvedException() unresolved
* exception}.
* is not thrown directly. Rather an {@link MvcTestResult} is available
* with an {@link MvcTestResult#getUnresolvedException() unresolved
* exception}. You can assert that a request has failed unexpectedly:
* <pre><code class='java'>
* // perform a GET on /hi and assert the response body is equal to Hello
* assertThat(mvc.perform(get("/boom")))
* .hasUnresolvedException())
* .withMessage("Test exception");
* </code></pre>
*
* <p>{@link AssertableMockMvc} can be configured with a list of
* <p>{@link MockMvcTester} can be configured with a list of
* {@linkplain HttpMessageConverter message converters} to allow the response
* body to be deserialized, rather than asserting on the raw values.
*
@@ -54,7 +85,7 @@ import org.springframework.web.context.WebApplicationContext;
* @author Brian Clozel
* @since 6.2
*/
public final class AssertableMockMvc {
public final class MockMvcTester {
private static final MediaType JSON = MediaType.APPLICATION_JSON;
@@ -64,23 +95,23 @@ public final class AssertableMockMvc {
private final GenericHttpMessageConverter<Object> jsonMessageConverter;
private AssertableMockMvc(MockMvc mockMvc, @Nullable GenericHttpMessageConverter<Object> jsonMessageConverter) {
private MockMvcTester(MockMvc mockMvc, @Nullable GenericHttpMessageConverter<Object> jsonMessageConverter) {
Assert.notNull(mockMvc, "mockMVC should not be null");
this.mockMvc = mockMvc;
this.jsonMessageConverter = jsonMessageConverter;
}
/**
* Create a {@link AssertableMockMvc} instance that delegates to the given
* Create a {@link MockMvcTester} instance that delegates to the given
* {@link MockMvc} instance.
* @param mockMvc the MockMvc instance to delegate calls to
*/
public static AssertableMockMvc create(MockMvc mockMvc) {
return new AssertableMockMvc(mockMvc, null);
public static MockMvcTester create(MockMvc mockMvc) {
return new MockMvcTester(mockMvc, null);
}
/**
* Create an {@link AssertableMockMvc} instance using the given, fully
* Create an {@link MockMvcTester} instance using the given, fully
* initialized (i.e., <em>refreshed</em>) {@link WebApplicationContext}. The
* given {@code customizations} are applied to the {@link DefaultMockMvcBuilder}
* that ultimately creates the underlying {@link MockMvc} instance.
@@ -92,7 +123,7 @@ public final class AssertableMockMvc {
* instance based on a {@link DefaultMockMvcBuilder}.
* @see MockMvcBuilders#webAppContextSetup(WebApplicationContext)
*/
public static AssertableMockMvc from(WebApplicationContext applicationContext,
public static MockMvcTester from(WebApplicationContext applicationContext,
Function<DefaultMockMvcBuilder, MockMvc> customizations) {
DefaultMockMvcBuilder builder = MockMvcBuilders.webAppContextSetup(applicationContext);
@@ -101,7 +132,7 @@ public final class AssertableMockMvc {
}
/**
* Shortcut to create an {@link AssertableMockMvc} instance using the given,
* Shortcut to create an {@link MockMvcTester} instance using the given,
* fully initialized (i.e., <em>refreshed</em>) {@link WebApplicationContext}.
* <p>Consider using {@link #from(WebApplicationContext, Function)} if
* further customization of the underlying {@link MockMvc} instance is
@@ -110,12 +141,12 @@ public final class AssertableMockMvc {
* MVC infrastructure and application controllers from
* @see MockMvcBuilders#webAppContextSetup(WebApplicationContext)
*/
public static AssertableMockMvc from(WebApplicationContext applicationContext) {
public static MockMvcTester from(WebApplicationContext applicationContext) {
return from(applicationContext, DefaultMockMvcBuilder::build);
}
/**
* Create an {@link AssertableMockMvc} instance by registering one or more
* Create an {@link MockMvcTester} instance by registering one or more
* {@code @Controller} instances and configuring Spring MVC infrastructure
* programmatically.
* <p>This allows full control over the instantiation and initialization of
@@ -129,7 +160,7 @@ public final class AssertableMockMvc {
* Spring MVC infrastructure
* @see MockMvcBuilders#standaloneSetup(Object...)
*/
public static AssertableMockMvc of(Collection<?> controllers,
public static MockMvcTester of(Collection<?> controllers,
Function<StandaloneMockMvcBuilder, MockMvc> customizations) {
StandaloneMockMvcBuilder builder = MockMvcBuilders.standaloneSetup(controllers.toArray());
@@ -137,8 +168,8 @@ public final class AssertableMockMvc {
}
/**
* Shortcut to create an {@link AssertableMockMvc} instance by registering
* one or more {@code @Controller} instances.
* Shortcut to create an {@link MockMvcTester} instance by registering one
* or more {@code @Controller} instances.
* <p>The minimum infrastructure required by the
* {@link org.springframework.web.servlet.DispatcherServlet DispatcherServlet}
* to serve requests with annotated controllers is created. Consider using
@@ -149,12 +180,12 @@ public final class AssertableMockMvc {
* into an instance
* @see MockMvcBuilders#standaloneSetup(Object...)
*/
public static AssertableMockMvc of(Object... controllers) {
public static MockMvcTester of(Object... controllers) {
return of(Arrays.asList(controllers), StandaloneMockMvcBuilder::build);
}
/**
* Return a new {@link AssertableMockMvc} instance using the specified
* Return a new {@link MockMvcTester} instance using the specified
* {@linkplain HttpMessageConverter message converters}.
* <p>If none are specified, only basic assertions on the response body can
* be performed. Consider registering a suitable JSON converter for asserting
@@ -162,13 +193,13 @@ public final class AssertableMockMvc {
* @param httpMessageConverters the message converters to use
* @return a new instance using the specified converters
*/
public AssertableMockMvc withHttpMessageConverters(Iterable<HttpMessageConverter<?>> httpMessageConverters) {
return new AssertableMockMvc(this.mockMvc, findJsonMessageConverter(httpMessageConverters));
public MockMvcTester withHttpMessageConverters(Iterable<HttpMessageConverter<?>> httpMessageConverters) {
return new MockMvcTester(this.mockMvc, findJsonMessageConverter(httpMessageConverters));
}
/**
* Perform a request and return a type that can be used with standard
* {@link org.assertj.core.api.Assertions AssertJ} assertions.
* Perform a request and return a {@link MvcTestResult result} that can be
* used with standard {@link org.assertj.core.api.Assertions AssertJ} assertions.
* <p>Use static methods of {@link MockMvcRequestBuilders} to prepare the
* request, wrapping the invocation in {@code assertThat}. The following
* asserts that a {@linkplain MockMvcRequestBuilders#get(URI) GET} request
@@ -191,16 +222,16 @@ public final class AssertableMockMvc {
* @param requestBuilder used to prepare the request to execute;
* see static factory methods in
* {@link org.springframework.test.web.servlet.request.MockMvcRequestBuilders}
* @return an {@link AssertableMvcResult} to be wrapped in {@code assertThat}
* @return an {@link MvcTestResult} to be wrapped in {@code assertThat}
* @see MockMvc#perform(RequestBuilder)
*/
public AssertableMvcResult perform(RequestBuilder requestBuilder) {
public MvcTestResult perform(RequestBuilder requestBuilder) {
Object result = getMvcResultOrFailure(requestBuilder);
if (result instanceof MvcResult mvcResult) {
return new DefaultAssertableMvcResult(mvcResult, null, this.jsonMessageConverter);
return new DefaultMvcTestResult(mvcResult, null, this.jsonMessageConverter);
}
else {
return new DefaultAssertableMvcResult(null, (Exception) result, this.jsonMessageConverter);
return new DefaultMvcTestResult(null, (Exception) result, this.jsonMessageConverter);
}
}

View File

@@ -22,23 +22,34 @@ import org.springframework.lang.Nullable;
import org.springframework.test.web.servlet.MvcResult;
/**
* A {@link MvcResult} that additionally supports AssertJ style assertions.
* Provide to the result of an executed request using {@link MockMvcTester} that
* is meant to be used with {@link org.assertj.core.api.Assertions#assertThat(AssertProvider)
* assertThat}.
*
* <p>Can be in one of two distinct states:
* <ol>
* <li>The request processed successfully, and {@link #getUnresolvedException()}
* is therefore {@code null}.</li>
* <li>The request processed successfully, even if it fails with an exception
* that has been resolved. {@link #getMvcResult()} is available and
* {@link #getUnresolvedException()} is {@code null}.</li>
* <li>The request failed unexpectedly with {@link #getUnresolvedException()}
* providing more information about the error. Any attempt to access a member of
* the result fails with an exception.</li>
* providing more information about the error. Any attempt to access
* {@link #getMvcResult() the result } fails with an exception.</li>
* </ol>
*
* @author Stephane Nicoll
* @author Brian Clozel
* @since 6.2
* @see AssertableMockMvc
* @see MockMvcTester
*/
public interface AssertableMvcResult extends MvcResult, AssertProvider<MvcResultAssert> {
public interface MvcTestResult extends AssertProvider<MvcTestResultAssert> {
/**
* Return the {@link MvcResult result} of the processing.
* <p>If the request has failed unexpectedly, this throws an
* {@link IllegalStateException}.
* @return the {@link MvcResult}
*/
MvcResult getMvcResult();
/**
* Return the exception that was thrown unexpectedly while processing the

View File

@@ -43,22 +43,22 @@ import org.springframework.web.servlet.ModelAndView;
/**
* AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied
* to {@link MvcResult}.
* to {@link MvcTestResult}.
*
* @author Stephane Nicoll
* @author Brian Clozel
* @since 6.2
*/
public class MvcResultAssert extends AbstractMockHttpServletResponseAssert<MvcResultAssert, AssertableMvcResult> {
public class MvcTestResultAssert extends AbstractMockHttpServletResponseAssert<MvcTestResultAssert, MvcTestResult> {
MvcResultAssert(AssertableMvcResult mvcResult, @Nullable GenericHttpMessageConverter<Object> jsonMessageConverter) {
super(jsonMessageConverter, mvcResult, MvcResultAssert.class);
MvcTestResultAssert(MvcTestResult actual, @Nullable GenericHttpMessageConverter<Object> jsonMessageConverter) {
super(jsonMessageConverter, actual, MvcTestResultAssert.class);
}
@Override
protected MockHttpServletResponse getResponse() {
checkHasNotFailedUnexpectedly();
return this.actual.getResponse();
getMvcResult();
return this.actual.getMvcResult().getResponse();
}
/**
@@ -76,8 +76,7 @@ public class MvcResultAssert extends AbstractMockHttpServletResponseAssert<MvcRe
* object that uses the {@link MockHttpServletRequest} as the object to test.
*/
public AbstractMockHttpServletRequestAssert<?> request() {
checkHasNotFailedUnexpectedly();
return new MockHttpRequestAssert(this.actual.getRequest());
return new MockHttpRequestAssert(getMvcResult().getRequest());
}
/**
@@ -85,8 +84,7 @@ public class MvcResultAssert extends AbstractMockHttpServletResponseAssert<MvcRe
* response's {@linkplain Cookie cookies} as the object to test.
*/
public CookieMapAssert cookies() {
checkHasNotFailedUnexpectedly();
return new CookieMapAssert(this.actual.getResponse().getCookies());
return new CookieMapAssert(getMvcResult().getResponse().getCookies());
}
/**
@@ -94,8 +92,7 @@ public class MvcResultAssert extends AbstractMockHttpServletResponseAssert<MvcRe
* response's {@linkplain MediaType content type} as the object to test.
*/
public MediaTypeAssert contentType() {
checkHasNotFailedUnexpectedly();
return new MediaTypeAssert(this.actual.getResponse().getContentType());
return new MediaTypeAssert(getMvcResult().getResponse().getContentType());
}
/**
@@ -108,8 +105,7 @@ public class MvcResultAssert extends AbstractMockHttpServletResponseAssert<MvcRe
* </code></pre>
*/
public HandlerResultAssert handler() {
checkHasNotFailedUnexpectedly();
return new HandlerResultAssert(this.actual.getHandler());
return new HandlerResultAssert(getMvcResult().getHandler());
}
/**
@@ -118,7 +114,6 @@ public class MvcResultAssert extends AbstractMockHttpServletResponseAssert<MvcRe
* {@linkplain ModelAndView#getModel() model} as the object to test.
*/
public ModelAssert model() {
checkHasNotFailedUnexpectedly();
return new ModelAssert(getModelAndView().getModel());
}
@@ -129,7 +124,6 @@ public class MvcResultAssert extends AbstractMockHttpServletResponseAssert<MvcRe
* @see #hasViewName(String)
*/
public AbstractStringAssert<?> viewName() {
checkHasNotFailedUnexpectedly();
return Assertions.assertThat(getModelAndView().getViewName()).as("View name");
}
@@ -139,8 +133,7 @@ public class MvcResultAssert extends AbstractMockHttpServletResponseAssert<MvcRe
* to test.
*/
public MapAssert<String, Object> flash() {
checkHasNotFailedUnexpectedly();
return new MapAssert<>(this.actual.getFlashMap());
return new MapAssert<>(getMvcResult().getFlashMap());
}
/**
@@ -151,14 +144,14 @@ public class MvcResultAssert extends AbstractMockHttpServletResponseAssert<MvcRe
*/
public ObjectAssert<Object> asyncResult() {
request().hasAsyncStarted(true);
return Assertions.assertThat(this.actual.getAsyncResult()).as("Async result");
return Assertions.assertThat(getMvcResult().getAsyncResult()).as("Async result");
}
/**
* Verify that the request has failed with an unresolved exception.
* @see #unresolvedException()
*/
public MvcResultAssert hasUnresolvedException() {
public MvcTestResultAssert hasUnresolvedException() {
Assertions.assertThat(this.actual.getUnresolvedException())
.withFailMessage("Expecting request to have failed but it has succeeded").isNotNull();
return this;
@@ -167,7 +160,7 @@ public class MvcResultAssert extends AbstractMockHttpServletResponseAssert<MvcRe
/**
* Verify that the request has not failed with an unresolved exception.
*/
public MvcResultAssert doesNotHaveUnresolvedException() {
public MvcTestResultAssert doesNotHaveUnresolvedException() {
Assertions.assertThat(this.actual.getUnresolvedException())
.withFailMessage("Expecting request to have succeeded but it has failed").isNull();
return this;
@@ -177,18 +170,18 @@ public class MvcResultAssert extends AbstractMockHttpServletResponseAssert<MvcRe
* Verify that the actual mvc result matches the given {@link ResultMatcher}.
* @param resultMatcher the result matcher to invoke
*/
public MvcResultAssert matches(ResultMatcher resultMatcher) {
checkHasNotFailedUnexpectedly();
return super.satisfies(resultMatcher::match);
public MvcTestResultAssert matches(ResultMatcher resultMatcher) {
MvcResult mvcResult = getMvcResult();
return super.satisfies(tmc -> resultMatcher.match(mvcResult));
}
/**
* Apply the given {@link ResultHandler} to the actual mvc result.
* @param resultHandler the result matcher to invoke
*/
public MvcResultAssert apply(ResultHandler resultHandler) {
checkHasNotFailedUnexpectedly();
return satisfies(resultHandler::handle);
public MvcTestResultAssert apply(ResultHandler resultHandler) {
MvcResult mvcResult = getMvcResult();
return satisfies(tmc -> resultHandler.handle(mvcResult));
}
/**
@@ -197,7 +190,7 @@ public class MvcResultAssert extends AbstractMockHttpServletResponseAssert<MvcRe
* {@link #viewName()}
* @param viewName the expected view name
*/
public MvcResultAssert hasViewName(String viewName) {
public MvcTestResultAssert hasViewName(String viewName) {
viewName().isEqualTo(viewName);
return this.myself;
}
@@ -205,17 +198,18 @@ public class MvcResultAssert extends AbstractMockHttpServletResponseAssert<MvcRe
@SuppressWarnings("NullAway")
private ModelAndView getModelAndView() {
ModelAndView modelAndView = this.actual.getModelAndView();
ModelAndView modelAndView = getMvcResult().getModelAndView();
Assertions.assertThat(modelAndView).as("ModelAndView").isNotNull();
return modelAndView;
}
protected void checkHasNotFailedUnexpectedly() {
protected MvcResult getMvcResult() {
Exception unresolvedException = this.actual.getUnresolvedException();
if (unresolvedException != null) {
throw Failures.instance().failure(this.info,
new RequestFailedUnexpectedly(unresolvedException));
}
return this.actual.getMvcResult();
}
private static final class MockHttpRequestAssert extends AbstractMockHttpServletRequestAssert<MockHttpRequestAssert> {