Add AssertJ support for MockMvc

This commit adds a new way to use MockMvc by returning a MvcResult that
is AssertJ compatible. Compared to the existing MockMvc infrastructure,
this new model has the following advantages:

* Can be created from a MockMvc instance
* Dedicated builder methods for easier setup
* Assertions use familiar AssertJ syntax with discoverable assertions
through a DSL
* Improved exception message

See gh-21178

Co-authored-by: Brian Clozel <brian.clozel@broadcom.com>
This commit is contained in:
Stéphane Nicoll
2024-03-15 13:30:16 +01:00
parent 555d4a6004
commit e7d7cb8641
8 changed files with 1536 additions and 0 deletions

View File

@@ -0,0 +1,227 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.web.servlet.assertj;
import java.net.URI;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.StreamSupport;
import org.springframework.http.MediaType;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.lang.Nullable;
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.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.test.web.servlet.setup.StandaloneMockMvcBuilder;
import org.springframework.util.Assert;
import org.springframework.web.context.WebApplicationContext;
/**
* {@link MockMvc} variant that tests Spring MVC exchanges and provide fluent
* assertions using {@link org.assertj.core.api.Assertions AssertJ}.
*
* <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}.
*
* <p>{@link AssertableMockMvc} can be configured with a list of
* {@linkplain HttpMessageConverter HttpMessageConverters} to allow response
* body to be deserialized, rather than asserting on the raw values.
*
* @author Stephane Nicoll
* @author Brian Clozel
* @since 6.2
*/
public final class AssertableMockMvc {
private static final MediaType JSON = MediaType.APPLICATION_JSON;
private final MockMvc mockMvc;
@Nullable
private final GenericHttpMessageConverter<Object> jsonMessageConverter;
private AssertableMockMvc(MockMvc mockMvc, @Nullable GenericHttpMessageConverter<Object> jsonMessageConverter) {
Assert.notNull(mockMvc, "mockMVC should not be null");
this.mockMvc = mockMvc;
this.jsonMessageConverter = jsonMessageConverter;
}
/**
* Create a new {@link AssertableMockMvc} instance that delegates to the
* given {@link MockMvc}.
* @param mockMvc the MockMvc instance to delegate calls to
*/
public static AssertableMockMvc create(MockMvc mockMvc) {
return new AssertableMockMvc(mockMvc, null);
}
/**
* Create a {@link AssertableMockMvc} 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.
* <p>If no further customization of the underlying {@link MockMvc} instance
* is required, use {@link #from(WebApplicationContext)}.
* @param applicationContext the application context to detect the Spring
* MVC infrastructure and application controllers from
* @param customizations the function that creates a {@link MockMvc}
* instance based on a {@link DefaultMockMvcBuilder}.
* @see MockMvcBuilders#webAppContextSetup(WebApplicationContext)
*/
public static AssertableMockMvc from(WebApplicationContext applicationContext,
Function<DefaultMockMvcBuilder, MockMvc> customizations) {
DefaultMockMvcBuilder builder = MockMvcBuilders.webAppContextSetup(applicationContext);
MockMvc mockMvc = customizations.apply(builder);
return create(mockMvc);
}
/**
* Shortcut to create a {@link AssertableMockMvc} instance using the given,
* fully initialized (i.e., <em>refreshed</em>) {@link WebApplicationContext}.
* <p>Consider using {@link #from(WebApplicationContext, Function)} if
* further customizations of the underlying {@link MockMvc} instance is
* required.
* @param applicationContext the application context to detect the Spring
* MVC infrastructure and application controllers from
* @see MockMvcBuilders#webAppContextSetup(WebApplicationContext)
*/
public static AssertableMockMvc from(WebApplicationContext applicationContext) {
return from(applicationContext, DefaultMockMvcBuilder::build);
}
/**
* Create a {@link AssertableMockMvc} 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
* controllers and their dependencies, similar to plain unit tests while
* also making it possible to test one controller at a time.
* @param controllers one or more {@code @Controller} instances to test
* (specified {@code Class} will be turned into instance)
* @param customizations the function that creates a {@link MockMvc}
* instance based on a {@link StandaloneMockMvcBuilder}, typically to
* configure the Spring MVC infrastructure
* @see MockMvcBuilders#standaloneSetup(Object...)
*/
public static AssertableMockMvc of(Collection<?> controllers,
Function<StandaloneMockMvcBuilder, MockMvc> customizations) {
StandaloneMockMvcBuilder builder = MockMvcBuilders.standaloneSetup(controllers.toArray());
return create(customizations.apply(builder));
}
/**
* Shortcut to create a {@link AssertableMockMvc} 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
* {@link #of(Collection, Function)} if additional configuration of the MVC
* infrastructure is required.
* @param controllers one or more {@code @Controller} instances to test
* (specified {@code Class} will be turned into instance)
* @see MockMvcBuilders#standaloneSetup(Object...)
*/
public static AssertableMockMvc of(Object... controllers) {
return of(Arrays.asList(controllers), StandaloneMockMvcBuilder::build);
}
/**
* Return a new {@link AssertableMockMvc} instance using the specified
* {@link HttpMessageConverter}. If none are specified, only basic assertions
* on the response body can be performed. Consider registering a suitable
* JSON converter for asserting data structure.
* @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));
}
/**
* Perform a request and return a type 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
* against "/greet" has an HTTP status code 200 (OK), and a simple body:
* <pre><code class='java'>assertThat(mvc.perform(get("/greet")))
* .hasStatusOk()
* .body().asString().isEqualTo("Hello");
* </code></pre>
* <p>Contrary to {@link MockMvc#perform(RequestBuilder)}, this does not
* throw an exception if the request fails with an unresolved exception.
* Rather, the result provides the exception, if any. Assuming that a
* {@linkplain MockMvcRequestBuilders#post(URI) POST} request against
* {@code /boom} throws an {@code IllegalStateException}, the following
* asserts that the invocation has indeed failed with the expected error
* message:
* <pre><code class='java'>assertThat(mvc.perform(post("/boom")))
* .unresolvedException().isInstanceOf(IllegalStateException.class)
* .hasMessage("Expected");
* </code></pre>
* <p>
* @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}
* @see MockMvc#perform(RequestBuilder)
*/
public AssertableMvcResult perform(RequestBuilder requestBuilder) {
Object result = getMvcResultOrFailure(requestBuilder);
if (result instanceof MvcResult mvcResult) {
return new DefaultAssertableMvcResult(mvcResult, null, this.jsonMessageConverter);
}
else {
return new DefaultAssertableMvcResult(null, (Exception) result, this.jsonMessageConverter);
}
}
private Object getMvcResultOrFailure(RequestBuilder requestBuilder) {
try {
return this.mockMvc.perform(requestBuilder).andReturn();
}
catch (Exception ex) {
return ex;
}
}
@SuppressWarnings("unchecked")
@Nullable
private GenericHttpMessageConverter<Object> findJsonMessageConverter(
Iterable<HttpMessageConverter<?>> messageConverters) {
return StreamSupport.stream(messageConverters.spliterator(), false)
.filter(GenericHttpMessageConverter.class::isInstance)
.map(GenericHttpMessageConverter.class::cast)
.filter(converter -> converter.canWrite(null, Map.class, JSON))
.filter(converter -> converter.canRead(Map.class, JSON))
.findFirst().orElse(null);
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.web.servlet.assertj;
import org.assertj.core.api.AssertProvider;
import org.springframework.lang.Nullable;
import org.springframework.test.web.servlet.MvcResult;
/**
* A {@link MvcResult} that additionally supports AssertJ style assertions.
*
* <p>Can be in two distinct states:
* <ol>
* <li>The request processed successfully, and {@link #getUnresolvedException()}
* is therefore {@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>
* </ol>
*
* @author Stephane Nicoll
* @author Brian Clozel
* @since 6.2
* @see AssertableMockMvc
*/
public interface AssertableMvcResult extends MvcResult, AssertProvider<MvcResultAssert> {
/**
* Return the exception that was thrown unexpectedly while processing the
* request, if any.
*/
@Nullable
Exception getUnresolvedException();
}

View File

@@ -0,0 +1,123 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.web.servlet.assertj;
import org.springframework.http.converter.GenericHttpMessageConverter;
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.
*
* @author Stephane Nicoll
* @since 6.2
*/
final class DefaultAssertableMvcResult implements AssertableMvcResult {
@Nullable
private final MvcResult target;
@Nullable
private final Exception unresolvedException;
@Nullable
private final GenericHttpMessageConverter<Object> jsonMessageConverter;
DefaultAssertableMvcResult(@Nullable MvcResult target, @Nullable Exception unresolvedException, @Nullable GenericHttpMessageConverter<Object> jsonMessageConverter) {
this.target = target;
this.unresolvedException = unresolvedException;
this.jsonMessageConverter = jsonMessageConverter;
}
/**
* Return the exception that was thrown unexpectedly while processing the
* request, if any.
*/
@Nullable
public Exception getUnresolvedException() {
return this.unresolvedException;
}
@Override
public MockHttpServletRequest getRequest() {
return getTarget().getRequest();
}
@Override
public MockHttpServletResponse getResponse() {
return getTarget().getResponse();
}
@Override
public Object getHandler() {
return getTarget().getHandler();
}
@Override
public HandlerInterceptor[] getInterceptors() {
return getTarget().getInterceptors();
}
@Override
public ModelAndView getModelAndView() {
return getTarget().getModelAndView();
}
@Override
public Exception getResolvedException() {
return getTarget().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);
}
}

View File

@@ -0,0 +1,258 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.web.servlet.assertj;
import java.io.BufferedReader;
import java.io.PrintWriter;
import java.io.StringReader;
import java.io.StringWriter;
import jakarta.servlet.http.Cookie;
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;
import org.springframework.http.MediaType;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.http.MediaTypeAssert;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultHandler;
import org.springframework.test.web.servlet.ResultMatcher;
import org.springframework.web.servlet.ModelAndView;
/**
* AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied
* to {@link MvcResult}.
*
* @author Stephane Nicoll
* @author Brian Clozel
* @since 6.2
*/
public class MvcResultAssert extends AbstractMockHttpServletResponseAssert<MvcResultAssert, AssertableMvcResult> {
MvcResultAssert(AssertableMvcResult mvcResult, @Nullable GenericHttpMessageConverter<Object> jsonMessageConverter) {
super(jsonMessageConverter, mvcResult, MvcResultAssert.class);
}
@Override
protected MockHttpServletResponse getResponse() {
checkHasNotFailedUnexpectedly();
return this.actual.getResponse();
}
/**
* Verify that the request has failed with an unresolved exception, and
* return a new {@linkplain AbstractThrowableAssert assertion} object
* that uses the unresolved {@link Exception} as the object to test.
*/
public AbstractThrowableAssert<?, ? extends Throwable> unresolvedException() {
hasUnresolvedException();
return Assertions.assertThat(this.actual.getUnresolvedException());
}
/**
* Return a new {@linkplain AbstractMockHttpServletRequestAssert assertion}
* object that uses the {@link MockHttpServletRequest} as the object to test.
*/
public AbstractMockHttpServletRequestAssert<?> request() {
checkHasNotFailedUnexpectedly();
return new MockHttpRequestAssert(this.actual.getRequest());
}
/**
* Return a new {@linkplain CookieMapAssert assertion} object that uses the
* response's {@linkplain Cookie cookies} as the object to test.
*/
public CookieMapAssert cookies() {
checkHasNotFailedUnexpectedly();
return new CookieMapAssert(this.actual.getResponse().getCookies());
}
/**
* Return a new {@linkplain MediaTypeAssert assertion} object that uses the
* response's {@linkplain MediaType content type} as the object to test.
*/
public MediaTypeAssert contentType() {
checkHasNotFailedUnexpectedly();
return new MediaTypeAssert(this.actual.getResponse().getContentType());
}
/**
* Return a new {@linkplain HandlerResultAssert assertion} object that uses
* the handler as the object to test. For a method invocation on a
* controller, this is relative method handler
* Example: <pre><code class='java'>
* // Check that a GET to "/greet" is invoked on a "handleGreet" method name
* assertThat(mvc.perform(get("/greet")).handler().method().hasName("sayGreet");
* </code></pre>
*/
public HandlerResultAssert handler() {
checkHasNotFailedUnexpectedly();
return new HandlerResultAssert(this.actual.getHandler());
}
/**
* Verify that a {@link ModelAndView} is available and return a new
* {@linkplain ModelAssert assertion} object that uses the
* {@linkplain ModelAndView#getModel() model} as the object to test.
*/
public ModelAssert model() {
checkHasNotFailedUnexpectedly();
return new ModelAssert(getModelAndView().getModel());
}
/**
* Verify that a {@link ModelAndView} is available and return a new
* {@linkplain AbstractStringAssert assertion} object that uses the
* {@linkplain ModelAndView#getViewName()} view name} as the object to test.
* @see #hasViewName(String)
*/
public AbstractStringAssert<?> viewName() {
checkHasNotFailedUnexpectedly();
return Assertions.assertThat(getModelAndView().getViewName()).as("View name");
}
/**
* Return a new {@linkplain MapAssert assertion} object that uses the
* "output" flash attributes saved during request processing as the object
* to test.
*/
public MapAssert<String, Object> flash() {
checkHasNotFailedUnexpectedly();
return new MapAssert<>(this.actual.getFlashMap());
}
/**
* Verify that an {@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(this.actual.getAsyncResult()).as("Async result");
}
/**
* Verify that the request has failed with an unresolved exception.
* @see #unresolvedException()
*/
public MvcResultAssert hasUnresolvedException() {
Assertions.assertThat(this.actual.getUnresolvedException())
.withFailMessage("Expecting request to have failed but it has succeeded").isNotNull();
return this;
}
/**
* Verify that the request has not failed with an unresolved exception.
*/
public MvcResultAssert doesNotHaveUnresolvedException() {
Assertions.assertThat(this.actual.getUnresolvedException())
.withFailMessage("Expecting request to have succeeded but it has failed").isNull();
return this;
}
/**
* 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);
}
/**
* 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);
}
/**
* Verify that a {@link ModelAndView} is available with a view equals to
* the given one. For more advanced assertions, consider using
* {@link #viewName()}
* @param viewName the expected view name
*/
public MvcResultAssert hasViewName(String viewName) {
viewName().isEqualTo(viewName);
return this.myself;
}
private ModelAndView getModelAndView() {
ModelAndView modelAndView = this.actual.getModelAndView();
Assertions.assertThat(modelAndView).as("ModelAndView").isNotNull();
return modelAndView;
}
protected void checkHasNotFailedUnexpectedly() {
Exception unresolvedException = this.actual.getUnresolvedException();
if (unresolvedException != null) {
throw Failures.instance().failure(this.info,
new RequestFailedUnexpectedly(unresolvedException));
}
}
private static final class MockHttpRequestAssert extends AbstractMockHttpServletRequestAssert<MockHttpRequestAssert> {
private MockHttpRequestAssert(MockHttpServletRequest request) {
super(request, MockHttpRequestAssert.class);
}
}
private static final class RequestFailedUnexpectedly extends BasicErrorMessageFactory {
private RequestFailedUnexpectedly(Exception ex) {
super("%nRequest has failed unexpectedly:%n%s", unquotedString(getIndentedStackTraceAsString(ex)));
}
private static String getIndentedStackTraceAsString(Throwable ex) {
String stackTrace = getStackTraceAsString(ex);
return indent(stackTrace);
}
private static String getStackTraceAsString(Throwable ex) {
StringWriter writer = new StringWriter();
PrintWriter printer = new PrintWriter(writer);
ex.printStackTrace(printer);
return writer.toString();
}
private static String indent(String input) {
BufferedReader reader = new BufferedReader(new StringReader(input));
StringWriter writer = new StringWriter();
PrintWriter printer = new PrintWriter(writer);
reader.lines().forEach(line -> {
printer.print(" ");
printer.println(line);
});
return writer.toString();
}
}
}

View File

@@ -0,0 +1,558 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.web.servlet.assertj;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Size;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.Person;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import static java.util.Map.entry;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.InstanceOfAssertFactories.map;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Integration tests for {@link AssertableMockMvc}.
*
* @author Brian Clozel
* @author Stephane Nicoll
*/
@SpringJUnitConfig
@WebAppConfiguration
public class AssertableMockMvcIntegrationTests {
private final AssertableMockMvc mockMvc;
AssertableMockMvcIntegrationTests(WebApplicationContext wac) {
this.mockMvc = AssertableMockMvc.from(wac);
}
@Nested
class RequestTests {
@Test
void hasAsyncStartedTrue() {
assertThat(perform(get("/callable").accept(MediaType.APPLICATION_JSON)))
.request().hasAsyncStarted(true);
}
@Test
void hasAsyncStartedFalse() {
assertThat(perform(get("/greet"))).request().hasAsyncStarted(false);
}
@Test
void attributes() {
assertThat(perform(get("/greet"))).request().attributes()
.containsKey(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE);
}
@Test
void sessionAttributes() {
assertThat(perform(get("/locale"))).request().sessionAttributes()
.containsOnly(entry("locale", Locale.UK));
}
}
@Nested
class CookieTests {
@Test
void containsCookie() {
Cookie cookie = new Cookie("test", "value");
assertThat(performWithCookie(cookie, get("/greet"))).cookies().containsCookie("test");
}
@Test
void hasValue() {
Cookie cookie = new Cookie("test", "value");
assertThat(performWithCookie(cookie, get("/greet"))).cookies().hasValue("test", "value");
}
private AssertableMvcResult performWithCookie(Cookie cookie, MockHttpServletRequestBuilder request) {
AssertableMockMvc mockMvc = AssertableMockMvc.of(List.of(new TestController()), builder -> builder.addInterceptors(
new HandlerInterceptor() {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
response.addCookie(cookie);
return true;
}
}).build());
return mockMvc.perform(request);
}
}
@Nested
class ContentTypeTests {
@Test
void contentType() {
assertThat(perform(get("/greet"))).contentType().isCompatibleWith("text/plain");
}
}
@Nested
class StatusTests {
@Test
void statusOk() {
assertThat(perform(get("/greet"))).hasStatusOk();
}
@Test
void statusSeries() {
assertThat(perform(get("/greet"))).hasStatus2xxSuccessful();
}
}
@Nested
class HeadersTests {
@Test
void shouldAssertHeader() {
assertThat(perform(get("/greet"))).headers()
.hasValue("Content-Type", "text/plain;charset=ISO-8859-1");
}
@Test
void shouldAssertHeaderWithCallback() {
assertThat(perform(get("/greet"))).headers().satisfies(textContent("ISO-8859-1"));
}
private Consumer<HttpHeaders> textContent(String charset) {
return headers -> assertThat(headers).containsEntry(
"Content-Type", List.of("text/plain;charset=%s".formatted(charset)));
}
}
@Nested
class ModelAndViewTests {
@Test
void hasViewName() {
assertThat(perform(get("/persons/{0}", "Andy"))).hasViewName("persons/index");
}
@Test
void viewNameWithCustomAssertion() {
assertThat(perform(get("/persons/{0}", "Andy"))).viewName().startsWith("persons");
}
@Test
void containsAttributes() {
assertThat(perform(post("/persons").param("name", "Andy"))).model()
.containsOnlyKeys("name").containsEntry("name", "Andy");
}
@Test
void hasErrors() {
assertThat(perform(post("/persons"))).model().hasErrors();
}
@Test
void hasAttributeErrors() {
assertThat(perform(post("/persons"))).model().hasAttributeErrors("person");
}
@Test
void hasAttributeErrorsCount() {
assertThat(perform(post("/persons"))).model().extractingBindingResult("person").hasErrorsCount(1);
}
}
@Nested
class FlashTests {
@Test
void containsAttributes() {
assertThat(perform(post("/persons").param("name", "Andy"))).flash()
.containsOnlyKeys("message").hasEntrySatisfying("message",
value -> assertThat(value).isInstanceOfSatisfying(String.class,
stringValue -> assertThat(stringValue).startsWith("success")));
}
}
@Nested
class BodyTests {
@Test
void asyncResult() {
assertThat(perform(get("/callable").accept(MediaType.APPLICATION_JSON)))
.asyncResult().asInstanceOf(map(String.class, Object.class))
.containsOnly(entry("key", "value"));
}
@Test
void stringContent() {
assertThat(perform(get("/greet"))).body().asString().isEqualTo("hello");
}
@Test
void jsonPathContent() {
assertThat(perform(get("/message"))).body().jsonPath()
.extractingPath("$.message").asString().isEqualTo("hello");
}
@Test
void jsonContentCanLoadResourceFromClasspath() {
assertThat(perform(get("/message"))).body().json().isLenientlyEqualTo(
new ClassPathResource("message.json", AssertableMockMvcIntegrationTests.class));
}
@Test
void jsonContentUsingResourceLoaderClass() {
assertThat(perform(get("/message"))).body().json(AssertableMockMvcIntegrationTests.class)
.isLenientlyEqualTo("message.json");
}
}
@Nested
class HandlerTests {
@Test
void handlerOn404() {
assertThat(perform(get("/unknown-resource"))).handler().isNull();
}
@Test
void hasType() {
assertThat(perform(get("/greet"))).handler().hasType(TestController.class);
}
@Test
void isMethodHandler() {
assertThat(perform(get("/greet"))).handler().isMethodHandler();
}
@Test
void isInvokedOn() {
assertThat(perform(get("/callable"))).handler()
.isInvokedOn(AsyncController.class, AsyncController::getCallable);
}
}
@Nested
class ExceptionTests {
@Test
void doesNotHaveUnresolvedException() {
assertThat(perform(get("/greet"))).doesNotHaveUnresolvedException();
}
@Test
void hasUnresolvedException() {
assertThat(perform(get("/error/1"))).hasUnresolvedException();
}
@Test
void doesNotHaveUnresolvedExceptionWithUnresolvedException() {
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
assertThat(perform(get("/error/1"))).doesNotHaveUnresolvedException())
.withMessage("Expecting request to have succeeded but it has failed");
}
@Test
void hasUnresolvedExceptionWithoutUnresolvedException() {
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
assertThat(perform(get("/greet"))).hasUnresolvedException())
.withMessage("Expecting request to have failed but it has succeeded");
}
@Test
void unresolvedExceptionWithFailedRequest() {
assertThat(perform(get("/error/1"))).unresolvedException()
.isInstanceOf(ServletException.class)
.cause().isInstanceOf(IllegalStateException.class).hasMessage("Expected");
}
@Test
void unresolvedExceptionWithSuccessfulRequest() {
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
assertThat(perform(get("/greet"))).unresolvedException())
.withMessage("Expecting request to have failed but it has succeeded");
}
// Check that assertions fail immediately if request has failed with unresolved exception
@Test
void assertAndApplyWithUnresolvedException() {
testAssertionFailureWithUnresolvableException(
result -> assertThat(result).apply(mvcResult -> {}));
}
@Test
void assertAsyncResultWithUnresolvedException() {
testAssertionFailureWithUnresolvableException(
result -> assertThat(result).asyncResult());
}
@Test
void assertContentTypeWithUnresolvedException() {
testAssertionFailureWithUnresolvableException(
result -> assertThat(result).contentType());
}
@Test
void assertCookiesWithUnresolvedException() {
testAssertionFailureWithUnresolvableException(
result -> assertThat(result).cookies());
}
@Test
void assertFlashWithUnresolvedException() {
testAssertionFailureWithUnresolvableException(
result -> assertThat(result).flash());
}
@Test
void assertStatusWithUnresolvedException() {
testAssertionFailureWithUnresolvableException(
result -> assertThat(result).hasStatus(3));
}
@Test
void assertHeaderWithUnresolvedException() {
testAssertionFailureWithUnresolvableException(
result -> assertThat(result).headers());
}
@Test
void assertViewNameWithUnresolvedException() {
testAssertionFailureWithUnresolvableException(
result -> assertThat(result).hasViewName("test"));
}
@Test
void assertForwardedUrlWithUnresolvedException() {
testAssertionFailureWithUnresolvableException(
result -> assertThat(result).hasForwardedUrl("test"));
}
@Test
void assertRedirectedUrlWithUnresolvedException() {
testAssertionFailureWithUnresolvableException(
result -> assertThat(result).hasRedirectedUrl("test"));
}
@Test
void assertRequestWithUnresolvedException() {
testAssertionFailureWithUnresolvableException(
result -> assertThat(result).request());
}
@Test
void assertModelWithUnresolvedException() {
testAssertionFailureWithUnresolvableException(
result -> assertThat(result).model());
}
@Test
void assertBodyWithUnresolvedException() {
testAssertionFailureWithUnresolvableException(
result -> assertThat(result).body());
}
private void testAssertionFailureWithUnresolvableException(Consumer<AssertableMvcResult> assertions) {
AssertableMvcResult result = perform(get("/error/1"));
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertions.accept(result))
.withMessageContainingAll("Request has failed unexpectedly:",
ServletException.class.getName(), IllegalStateException.class.getName(),
"Expected");
}
}
@Test
void hasForwardUrl() {
assertThat(perform(get("/persons/John"))).hasForwardedUrl("persons/index");
}
@Test
void hasRedirectUrl() {
assertThat(perform(post("/persons").param("name", "Andy"))).hasStatus(HttpStatus.FOUND)
.hasRedirectedUrl("/persons/Andy");
}
@Test
void satisfiesAllowAdditionalAssertions() {
assertThat(this.mockMvc.perform(get("/greet"))).satisfies(result -> {
assertThat(result).isInstanceOf(MvcResult.class);
assertThat(result).hasStatusOk();
});
}
@Test
void resultMatcherCanBeReused() {
assertThat(this.mockMvc.perform(get("/greet"))).matches(status().isOk());
}
@Test
void resultMatcherFailsWithDedicatedException() {
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(this.mockMvc.perform(get("/greet")))
.matches(status().isNotFound()))
.withMessageContaining("Status expected:<404> but was:<200>");
}
@Test
void shouldApplyResultHandler() { // Spring RESTDocs example
AtomicBoolean applied = new AtomicBoolean();
assertThat(this.mockMvc.perform(get("/greet"))).apply(result -> applied.set(true));
assertThat(applied).isTrue();
}
private AssertableMvcResult perform(MockHttpServletRequestBuilder builder) {
return this.mockMvc.perform(builder);
}
@Configuration
@EnableWebMvc
@Import({ TestController.class, PersonController.class, AsyncController.class,
SessionController.class, ErrorController.class })
static class WebConfiguration {
}
@RestController
static class TestController {
@GetMapping(path = "/greet", produces = "text/plain")
String greet() {
return "hello";
}
@GetMapping(path = "/message", produces = MediaType.APPLICATION_JSON_VALUE)
String message() {
return "{\"message\": \"hello\"}";
}
}
@Controller
@RequestMapping("/persons")
static class PersonController {
@GetMapping("/{name}")
public String get(@PathVariable String name, Model model) {
model.addAttribute(new Person(name));
return "persons/index";
}
@PostMapping
String create(@Valid Person person, Errors errors, RedirectAttributes redirectAttrs) {
if (errors.hasErrors()) {
return "persons/add";
}
redirectAttrs.addAttribute("name", person.getName());
redirectAttrs.addFlashAttribute("message", "success!");
return "redirect:/persons/{name}";
}
}
@RestController
static class AsyncController {
@GetMapping("/callable")
public Callable<Map<String, String>> getCallable() {
return () -> Collections.singletonMap("key", "value");
}
}
@Controller
@SessionAttributes("locale")
private static class SessionController {
@ModelAttribute
void populate(Model model) {
model.addAttribute("locale", Locale.UK);
}
@RequestMapping("/locale")
String handle() {
return "view";
}
}
@Controller
private static class ErrorController {
@GetMapping("/error/1")
public String one() {
throw new IllegalStateException("Expected");
}
@GetMapping("/error/validation/{id}")
public String validation(@PathVariable @Size(max = 4) String id) {
return "Hello " + id;
}
}
}

View File

@@ -0,0 +1,210 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.web.servlet.assertj;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.mock.web.MockServletContext;
import org.springframework.test.json.JsonPathAssert;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.support.GenericWebApplicationContext;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
/**
* Tests for {@link AssertableMockMvc}.
*
* @author Stephane Nicoll
*/
class AssertableMockMvcTests {
private static final MappingJackson2HttpMessageConverter jsonHttpMessageConverter =
new MappingJackson2HttpMessageConverter(new ObjectMapper());
@Test
void createShouldRejectNullMockMvc() {
assertThatThrownBy(() -> AssertableMockMvc.create(null))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
void createWithExistingWebApplicationContext() {
try (GenericWebApplicationContext wac = create(WebConfiguration.class)) {
AssertableMockMvc mockMvc = AssertableMockMvc.from(wac);
assertThat(mockMvc.perform(post("/increase"))).body().isEqualTo("counter 41");
assertThat(mockMvc.perform(post("/increase"))).body().isEqualTo("counter 42");
}
}
@Test
void createWithControllerClassShouldInstantiateControllers() {
AssertableMockMvc mockMvc = AssertableMockMvc.of(HelloController.class, CounterController.class);
assertThat(mockMvc.perform(get("/hello"))).body().isEqualTo("Hello World");
assertThat(mockMvc.perform(post("/increase"))).body().isEqualTo("counter 1");
assertThat(mockMvc.perform(post("/increase"))).body().isEqualTo("counter 2");
}
@Test
void createWithControllersShouldUseThemAsIs() {
AssertableMockMvc mockMvc = AssertableMockMvc.of(new HelloController(),
new CounterController(new AtomicInteger(41)));
assertThat(mockMvc.perform(get("/hello"))).body().isEqualTo("Hello World");
assertThat(mockMvc.perform(post("/increase"))).body().isEqualTo("counter 42");
assertThat(mockMvc.perform(post("/increase"))).body().isEqualTo("counter 43");
}
@Test
void createWithControllerAndCustomizations() {
AssertableMockMvc mockMvc = AssertableMockMvc.of(List.of(new HelloController()), builder ->
builder.defaultRequest(get("/hello").accept(MediaType.APPLICATION_JSON)).build());
assertThat(mockMvc.perform(get("/hello"))).hasStatus(HttpStatus.NOT_ACCEPTABLE);
}
@Test
void createWithControllersHasNoHttpMessageConverter() {
AssertableMockMvc mockMvc = AssertableMockMvc.of(new HelloController());
JsonPathAssert jsonPathAssert = assertThat(mockMvc.perform(get("/json"))).hasStatusOk().body().jsonPath();
assertThatIllegalStateException()
.isThrownBy(() -> jsonPathAssert.extractingPath("$").convertTo(Message.class))
.withMessageContaining("No JSON message converter available");
}
@Test
void createWithControllerCanConfigureHttpMessageConverters() {
AssertableMockMvc mockMvc = AssertableMockMvc.of(HelloController.class)
.withHttpMessageConverters(List.of(jsonHttpMessageConverter));
assertThat(mockMvc.perform(get("/json"))).hasStatusOk().body().jsonPath()
.extractingPath("$").convertTo(Message.class).satisfies(message -> {
assertThat(message.message()).isEqualTo("Hello World");
assertThat(message.counter()).isEqualTo(42);
});
}
@Test
@SuppressWarnings("unchecked")
void withHttpMessageConverterDetectsJsonConverter() {
MappingJackson2HttpMessageConverter converter = spy(jsonHttpMessageConverter);
AssertableMockMvc mockMvc = AssertableMockMvc.of(HelloController.class)
.withHttpMessageConverters(List.of(mock(GenericHttpMessageConverter.class),
mock(GenericHttpMessageConverter.class), converter));
assertThat(mockMvc.perform(get("/json"))).hasStatusOk().body().jsonPath()
.extractingPath("$").convertTo(Message.class).satisfies(message -> {
assertThat(message.message()).isEqualTo("Hello World");
assertThat(message.counter()).isEqualTo(42);
});
verify(converter).canWrite(Map.class, MediaType.APPLICATION_JSON);
}
@Test
void performWithUnresolvedExceptionSetsException() {
AssertableMockMvc mockMvc = AssertableMockMvc.of(HelloController.class);
AssertableMvcResult result = mockMvc.perform(get("/error"));
assertThat(result.getUnresolvedException()).isNotNull().isInstanceOf(ServletException.class)
.cause().isInstanceOf(IllegalStateException.class).hasMessage("Expected");
assertThat(result).hasFieldOrPropertyWithValue("target", null);
}
private GenericWebApplicationContext create(Class<?>... classes) {
GenericWebApplicationContext applicationContext = new GenericWebApplicationContext(
new MockServletContext());
AnnotationConfigUtils.registerAnnotationConfigProcessors(applicationContext);
for (Class<?> beanClass : classes) {
applicationContext.registerBean(beanClass);
}
applicationContext.refresh();
return applicationContext;
}
@Configuration(proxyBeanMethods = false)
@EnableWebMvc
static class WebConfiguration {
@Bean
CounterController counterController() {
return new CounterController(new AtomicInteger(40));
}
}
@RestController
private static class HelloController {
@GetMapping(path = "/hello", produces = "text/plain")
public String hello() {
return "Hello World";
}
@GetMapping("/error")
public String error() {
throw new IllegalStateException("Expected");
}
@GetMapping(path = "/json", produces = "application/json")
public String json() {
return """
{
"message": "Hello World",
"counter": 42
}""";
}
}
private record Message(String message, int counter) {}
@RestController
private static class CounterController {
private final AtomicInteger counter;
public CounterController(AtomicInteger counter) {
this.counter = counter;
}
public CounterController() {
this(new AtomicInteger());
}
@PostMapping("/increase")
public String increase() {
int value = this.counter.incrementAndGet();
return "counter " + value;
}
}
}

View File

@@ -0,0 +1,107 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.web.servlet.assertj;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.test.web.servlet.MvcResult;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link DefaultAssertableMvcResult}.
*
* @author Stephane Nicoll
*/
class DefaultAssertableMvcResultTests {
@Test
void createWithMvcResultDelegatesToIt() {
MockHttpServletRequest request = new MockHttpServletRequest();
MvcResult mvcResult = mock(MvcResult.class);
given(mvcResult.getRequest()).willReturn(request);
DefaultAssertableMvcResult result = new DefaultAssertableMvcResult(mvcResult, null, null);
assertThat(result.getRequest()).isSameAs(request);
verify(mvcResult).getRequest();
}
@Test
void createWithExceptionDoesNotAllowAccessToRequest() {
assertRequestHasFailed(DefaultAssertableMvcResult::getRequest);
}
@Test
void createWithExceptionDoesNotAllowAccessToResponse() {
assertRequestHasFailed(DefaultAssertableMvcResult::getResponse);
}
@Test
void createWithExceptionDoesNotAllowAccessToHandler() {
assertRequestHasFailed(DefaultAssertableMvcResult::getHandler);
}
@Test
void createWithExceptionDoesNotAllowAccessToInterceptors() {
assertRequestHasFailed(DefaultAssertableMvcResult::getInterceptors);
}
@Test
void createWithExceptionDoesNotAllowAccessToModelAndView() {
assertRequestHasFailed(DefaultAssertableMvcResult::getModelAndView);
}
@Test
void createWithExceptionDoesNotAllowAccessToResolvedException() {
assertRequestHasFailed(DefaultAssertableMvcResult::getResolvedException);
}
@Test
void createWithExceptionDoesNotAllowAccessToFlashMap() {
assertRequestHasFailed(DefaultAssertableMvcResult::getFlashMap);
}
@Test
void createWithExceptionDoesNotAllowAccessToAsyncResult() {
assertRequestHasFailed(DefaultAssertableMvcResult::getAsyncResult);
}
@Test
void createWithExceptionDoesNotAllowAccessToAsyncResultWithTimeToWait() {
assertRequestHasFailed(result -> result.getAsyncResult(1000));
}
@Test
void createWithExceptionReturnsException() {
IllegalStateException exception = new IllegalStateException("Expected");
DefaultAssertableMvcResult result = new DefaultAssertableMvcResult(null, exception, null);
assertThat(result.getUnresolvedException()).isSameAs(exception);
}
private void assertRequestHasFailed(Consumer<DefaultAssertableMvcResult> action) {
DefaultAssertableMvcResult result = new DefaultAssertableMvcResult(null, new IllegalStateException("Expected"), null);
assertThatIllegalStateException().isThrownBy(() -> action.accept(result))
.withMessageContaining("Request has failed with unresolved exception");
}
}

View File

@@ -0,0 +1,3 @@
{
"message": "hello"
}