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:
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user