From 74f58198fd689408973363d00a730b5c2bf4ed14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Tue, 3 Jan 2023 18:26:26 +0100 Subject: [PATCH] Add Kotlin DSL support for MockMVC andExpectAll (#29727) As the DSL internally calls `ResultActions.andExpect`, this is done with a trick where a synthetic `ResultActions` is provided at top level which stores each `ResultMatcher` in a mutable list. Once the DSL usage is done, the top level DSL `andExpectAll` turns that list into a `vararg` passed down to the actual `actions.andExpectAll`. Closes gh-27317 --- .../web/servlet/MockMvcResultMatchersDsl.kt | 8 +++++ .../test/web/servlet/ResultActionsDsl.kt | 29 +++++++++++++++ .../web/servlet/MockMvcExtensionsTests.kt | 36 +++++++++++++++++++ 3 files changed, 73 insertions(+) diff --git a/spring-test/src/main/kotlin/org/springframework/test/web/servlet/MockMvcResultMatchersDsl.kt b/spring-test/src/main/kotlin/org/springframework/test/web/servlet/MockMvcResultMatchersDsl.kt index 8971a80f4b5..593f666ec2e 100644 --- a/spring-test/src/main/kotlin/org/springframework/test/web/servlet/MockMvcResultMatchersDsl.kt +++ b/spring-test/src/main/kotlin/org/springframework/test/web/servlet/MockMvcResultMatchersDsl.kt @@ -145,4 +145,12 @@ class MockMvcResultMatchersDsl internal constructor (private val actions: Result fun match(matcher: ResultMatcher) { actions.andExpect(matcher) } + + /** + * @since 6.0.4 + * @see ResultActions.andExpectAll + */ + fun matchAll(vararg matchers: ResultMatcher) { + actions.andExpectAll(*matchers) + } } diff --git a/spring-test/src/main/kotlin/org/springframework/test/web/servlet/ResultActionsDsl.kt b/spring-test/src/main/kotlin/org/springframework/test/web/servlet/ResultActionsDsl.kt index 128b9492b8c..5d413702467 100644 --- a/spring-test/src/main/kotlin/org/springframework/test/web/servlet/ResultActionsDsl.kt +++ b/spring-test/src/main/kotlin/org/springframework/test/web/servlet/ResultActionsDsl.kt @@ -19,6 +19,35 @@ class ResultActionsDsl internal constructor (private val actions: ResultActions, return this } + + /** + * Provide access to [MockMvcResultMatchersDsl] Kotlin DSL. + * @since 6.0.4 + * @see MockMvcResultMatchersDsl.matchAll + */ + fun andExpectAll(dsl: MockMvcResultMatchersDsl.() -> Unit): ResultActionsDsl { + val softMatchers = mutableListOf() + val softActions = object : ResultActions { + override fun andExpect(matcher: ResultMatcher): ResultActions { + softMatchers.add(matcher) + return this + } + + override fun andDo(handler: ResultHandler): ResultActions { + throw UnsupportedOperationException("andDo should not be part of andExpectAll DSL calls") + } + + override fun andReturn(): MvcResult { + throw UnsupportedOperationException("andReturn should not be part of andExpectAll DSL calls") + } + + } + // the use of softActions as the matchers DSL actions parameter will store ResultMatchers in list + MockMvcResultMatchersDsl(softActions).dsl() + actions.andExpectAll(*softMatchers.toTypedArray()) + return this; + } + /** * Provide access to [MockMvcResultHandlersDsl] Kotlin DSL. * @see MockMvcResultHandlersDsl.handle diff --git a/spring-test/src/test/kotlin/org/springframework/test/web/servlet/MockMvcExtensionsTests.kt b/spring-test/src/test/kotlin/org/springframework/test/web/servlet/MockMvcExtensionsTests.kt index d1e731f3bc2..737e235d4e3 100644 --- a/spring-test/src/test/kotlin/org/springframework/test/web/servlet/MockMvcExtensionsTests.kt +++ b/spring-test/src/test/kotlin/org/springframework/test/web/servlet/MockMvcExtensionsTests.kt @@ -17,6 +17,7 @@ package org.springframework.test.web.servlet import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatCode import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.hamcrest.CoreMatchers import org.junit.jupiter.api.Test @@ -25,6 +26,7 @@ import org.springframework.http.HttpStatus import org.springframework.http.MediaType.APPLICATION_ATOM_XML import org.springframework.http.MediaType.APPLICATION_JSON import org.springframework.http.MediaType.APPLICATION_XML +import org.springframework.http.MediaType.TEXT_PLAIN import org.springframework.test.web.Person import org.springframework.test.web.servlet.setup.MockMvcBuilders import org.springframework.web.bind.annotation.GetMapping @@ -97,6 +99,24 @@ class MockMvcExtensionsTests { assertThat(handlerInvoked).isTrue() } + @Test + fun `request with two custom matchers and matchAll`() { + var matcher1Invoked = false + var matcher2Invoked = false + val matcher1 = ResultMatcher { matcher1Invoked = true; throw AssertionError("expected") } + val matcher2 = ResultMatcher { matcher2Invoked = true } + assertThatExceptionOfType(AssertionError::class.java).isThrownBy { + mockMvc.request(HttpMethod.GET, "/person/{name}", "Lee") + .andExpect { + matchAll(matcher1, matcher2) + } + } + .withMessage("expected") + + assertThat(matcher1Invoked).describedAs("matcher1").isTrue() + assertThat(matcher2Invoked).describedAs("matcher2").isTrue() + } + @Test fun get() { mockMvc.get("/person/{name}", "Lee") { @@ -183,6 +203,22 @@ class MockMvcExtensionsTests { } } + @Test + fun `andExpectAll reports multiple assertion errors`() { + assertThatCode { + mockMvc.request(HttpMethod.GET, "/person/{name}", "Lee") { + accept = APPLICATION_JSON + }.andExpectAll { + status { is4xxClientError() } + content { contentType(TEXT_PLAIN) } + jsonPath("$.name") { value("Lee") } + } + } + .hasMessage("Multiple Exceptions (2):\n" + + "Range for response status value 200 expected: but was:\n" + + "Content type expected: but was:") + } + @RestController private class PersonController {