From 0c332286f167ee5a431be8b2705a9178c273e14e Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Fri, 22 Feb 2019 13:16:52 +0100 Subject: [PATCH] Add MockMvc Kotlin DSL This commit introduces a `MockMvc` Kotlin DSL via a set of extensions like `MockMvc.get` or `MockMvc.request` that provide a Kotlin idiomatic and easily discoverable API while staying close to the original Java API design. Closes gh-1951 --- .../web/servlet/MockHttpServletRequestDsl.kt | 208 +++++++++++++++++ .../MockMultipartHttpServletRequestDsl.kt | 46 ++++ .../test/web/servlet/MockMvcExtensions.kt | 213 ++++++++++++++++++ .../web/servlet/MockMvcResultHandlersDsl.kt | 65 ++++++ .../web/servlet/MockMvcResultMatchersDsl.kt | 149 ++++++++++++ .../test/web/servlet/ResultActionsDsl.kt | 33 +++ .../web/servlet/MockMvcExtensionsTests.kt | 172 ++++++++++++++ src/docs/asciidoc/languages/kotlin.adoc | 25 ++ 8 files changed, 911 insertions(+) create mode 100644 spring-test/src/main/kotlin/org/springframework/test/web/servlet/MockHttpServletRequestDsl.kt create mode 100644 spring-test/src/main/kotlin/org/springframework/test/web/servlet/MockMultipartHttpServletRequestDsl.kt create mode 100644 spring-test/src/main/kotlin/org/springframework/test/web/servlet/MockMvcExtensions.kt create mode 100644 spring-test/src/main/kotlin/org/springframework/test/web/servlet/MockMvcResultHandlersDsl.kt create mode 100644 spring-test/src/main/kotlin/org/springframework/test/web/servlet/MockMvcResultMatchersDsl.kt create mode 100644 spring-test/src/main/kotlin/org/springframework/test/web/servlet/ResultActionsDsl.kt create mode 100644 spring-test/src/test/kotlin/org/springframework/test/web/servlet/MockMvcExtensionsTests.kt diff --git a/spring-test/src/main/kotlin/org/springframework/test/web/servlet/MockHttpServletRequestDsl.kt b/spring-test/src/main/kotlin/org/springframework/test/web/servlet/MockHttpServletRequestDsl.kt new file mode 100644 index 0000000000..10a4d7bcb4 --- /dev/null +++ b/spring-test/src/main/kotlin/org/springframework/test/web/servlet/MockHttpServletRequestDsl.kt @@ -0,0 +1,208 @@ +/* + * Copyright 2002-2019 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 + * + * http://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 + +import org.springframework.http.HttpHeaders +import org.springframework.http.MediaType +import org.springframework.mock.web.MockHttpSession +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder +import org.springframework.test.web.servlet.request.RequestPostProcessor +import org.springframework.util.MultiValueMap +import java.security.Principal +import java.util.* +import javax.servlet.http.Cookie + +/** + * Provide a [MockHttpServletRequestBuilder] Kotlin DSL in order to be able to write idiomatic Kotlin code. + * + * @see MockMvc.get + * @see MockMvc.post + * @see MockMvc.put + * @see MockMvc.patch + * @see MockMvc.delete + * @see MockMvc.options + * @see MockMvc.request + * @author Sebastien Deleuze + * @since 5.2 + */ +open class MockHttpServletRequestDsl(private val builder: MockHttpServletRequestBuilder) { + + /** + * @see [MockHttpServletRequestBuilder.contextPath] + */ + var contextPath: String? = null + + /** + * @see [MockHttpServletRequestBuilder.servletPath] + */ + var servletPath: String? = null + + /** + * @see [MockHttpServletRequestBuilder.pathInfo] + */ + var pathInfo: String? = null + + /** + * @see [MockHttpServletRequestBuilder.secure] + */ + var secure: Boolean? = null + + /** + * @see [MockHttpServletRequestBuilder.characterEncoding] + */ + var characterEncoding: String? = null + + /** + * @see [MockHttpServletRequestBuilder.content] + */ + var content: Any? = null + + /** + * @see [MockHttpServletRequestBuilder.accept] + */ + var accept: MediaType? = null + + /** + * @see [MockHttpServletRequestBuilder.accept] + */ + fun accept(vararg mediaTypes: MediaType) { + builder.accept(*mediaTypes) + } + + /** + * @see [MockHttpServletRequestBuilder.contentType] + */ + var contentType: MediaType? = null + + /** + * @see [MockHttpServletRequestBuilder.headers] + */ + fun headers(headers: HttpHeaders.() -> Unit) { + builder.headers(HttpHeaders().apply(headers)) + } + + /** + * @see [MockHttpServletRequestBuilder.header] + */ + fun header(name: String, vararg values: Any) { + builder.header(name, *values) + } + + /** + * @see [MockHttpServletRequestBuilder.param] + */ + fun param(name: String, vararg values: String) { + builder.param(name, *values) + } + + /** + * @see [MockHttpServletRequestBuilder.params] + */ + var params: MultiValueMap? = null + + /** + * @see [MockHttpServletRequestBuilder.cookie] + */ + fun cookie(vararg cookies: Cookie) { + builder.cookie(*cookies) + } + + /** + * @see [MockHttpServletRequestBuilder.locale] + */ + fun locale(vararg locales: Locale) { + builder.locale(*locales) + } + + /** + * @see [MockHttpServletRequestBuilder.requestAttr] + */ + fun requestAttr(name: String, value: Any) { + builder.requestAttr(name, value) + } + + /** + * @see [MockHttpServletRequestBuilder.sessionAttr] + */ + fun sessionAttr(name: String, value: Any) { + builder.sessionAttr(name, value) + } + + /** + * @see [MockHttpServletRequestBuilder.sessionAttrs] + */ + var sessionAttrs: Map? = null + + /** + * @see [MockHttpServletRequestBuilder.flashAttr] + */ + fun flashAttr(name: String, value: Any) { + builder.flashAttr(name, value) + } + + /** + * @see [MockHttpServletRequestBuilder.flashAttrs] + */ + var flashAttrs: Map? = null + + /** + * @see [MockHttpServletRequestBuilder.session] + */ + var session: MockHttpSession? = null + + /** + * @see [MockHttpServletRequestBuilder.principal] + */ + var principal: Principal? = null + + /** + * @see [MockHttpServletRequestBuilder.with] + */ + fun with(processor: RequestPostProcessor) { + builder.with(processor) + } + + /** + * @see [MockHttpServletRequestBuilder.merge] + */ + fun merge(parent: MockHttpServletRequestBuilder?) { + builder.merge(parent) + } + + internal fun perform(mockMvc: MockMvc): ResultActionsDsl { + contextPath?.also { builder.contextPath(contextPath!!) } + servletPath?.also { builder.servletPath(servletPath!!) } + pathInfo?.also { builder.pathInfo(pathInfo) } + secure?.also { builder.secure(secure!!) } + characterEncoding?.also { builder.characterEncoding(characterEncoding!!) } + content?.also { + when (content) { + is String -> builder.content(content as String) + is ByteArray -> builder.content(content as ByteArray) + else -> builder.content(content.toString()) + } + } + accept?.also { builder.accept(accept!!) } + contentType?.also { builder.contentType(contentType!!) } + params?.also { builder.params(params!!) } + sessionAttrs?.also { builder.sessionAttrs(sessionAttrs!!) } + flashAttrs?.also { builder.flashAttrs(flashAttrs!!) } + session?.also { builder.session(session!!) } + principal?.also { builder.principal(principal!!) } + return ResultActionsDsl(mockMvc.perform(builder)) + } +} diff --git a/spring-test/src/main/kotlin/org/springframework/test/web/servlet/MockMultipartHttpServletRequestDsl.kt b/spring-test/src/main/kotlin/org/springframework/test/web/servlet/MockMultipartHttpServletRequestDsl.kt new file mode 100644 index 0000000000..7590830972 --- /dev/null +++ b/spring-test/src/main/kotlin/org/springframework/test/web/servlet/MockMultipartHttpServletRequestDsl.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2019 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 + * + * http://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 + +import org.springframework.mock.web.MockMultipartFile +import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder +import javax.servlet.http.Part + +/** + * Provide a [MockMultipartHttpServletRequestBuilder] Kotlin DSL in order to be able to write idiomatic Kotlin code. + * + * @see MockMvc.multipart + * @author Sebastien Deleuze + * @since 5.2 + */ +class MockMultipartHttpServletRequestDsl(private val builder: MockMultipartHttpServletRequestBuilder) : MockHttpServletRequestDsl(builder) { + + /** + * @see [MockMultipartHttpServletRequestBuilder.file] + */ + fun file(name: String, content: ByteArray) = builder.file(name, content) + + /** + * @see [MockMultipartHttpServletRequestBuilder.file] + */ + fun file(file: MockMultipartFile) = builder.file(file) + + /** + * @see [MockMultipartHttpServletRequestBuilder.part] + */ + fun part(vararg parts: Part) = builder.part(*parts) +} diff --git a/spring-test/src/main/kotlin/org/springframework/test/web/servlet/MockMvcExtensions.kt b/spring-test/src/main/kotlin/org/springframework/test/web/servlet/MockMvcExtensions.kt new file mode 100644 index 0000000000..4df94c0b5b --- /dev/null +++ b/spring-test/src/main/kotlin/org/springframework/test/web/servlet/MockMvcExtensions.kt @@ -0,0 +1,213 @@ +/* + * Copyright 2002-2019 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 + * + * http://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 + +import org.springframework.http.HttpMethod +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders +import java.net.URI + +/** + * [MockMvc] extension providing access to [MockHttpServletRequestDsl] Kotlin DSL. + * + * @see MockMvcRequestBuilders.get + * @author Sebastien Deleuze + * @since 5.2 + */ +fun MockMvc.get(urlTemplate: String, vararg vars: Any?, dsl: MockHttpServletRequestDsl.() -> Unit = {}): ResultActionsDsl { + val requestBuilder = MockMvcRequestBuilders.get(urlTemplate, *vars) + return MockHttpServletRequestDsl(requestBuilder).apply(dsl).perform(this) +} + +/** + * [MockMvc] extension providing access to [MockHttpServletRequestDsl] Kotlin DSL. + * + * @see MockMvcRequestBuilders.get + * @author Sebastien Deleuze + * @since 5.2 + */ +fun MockMvc.get(uri: URI, dsl: MockHttpServletRequestDsl.() -> Unit = {}): ResultActionsDsl { + val requestBuilder = MockMvcRequestBuilders.get(uri) + return MockHttpServletRequestDsl(requestBuilder).apply(dsl).perform(this) +} + +/** + * [MockMvc] extension providing access to [MockHttpServletRequestDsl] Kotlin DSL. + * + * @see MockMvcRequestBuilders.post + * @author Sebastien Deleuze + * @since 5.2 + */ +fun MockMvc.post(urlTemplate: String, vararg vars: Any?, dsl: MockHttpServletRequestDsl.() -> Unit = {}): ResultActionsDsl { + val requestBuilder = MockMvcRequestBuilders.post(urlTemplate, *vars) + return MockHttpServletRequestDsl(requestBuilder).apply(dsl).perform(this) +} + +/** + * [MockMvc] extension providing access to [MockHttpServletRequestDsl] Kotlin DSL. + * + * @see MockMvcRequestBuilders.post + * @author Sebastien Deleuze + * @since 5.2 + */ +fun MockMvc.post(uri: URI, dsl: MockHttpServletRequestDsl.() -> Unit = {}): ResultActionsDsl { + val requestBuilder = MockMvcRequestBuilders.post(uri) + return MockHttpServletRequestDsl(requestBuilder).apply(dsl).perform(this) +} + +/** + * [MockMvc] extension providing access to [MockHttpServletRequestDsl] Kotlin DSL. + * + * @see MockMvcRequestBuilders.put + * @author Sebastien Deleuze + * @since 5.2 + */ +fun MockMvc.put(urlTemplate: String, vararg vars: Any?, dsl: MockHttpServletRequestDsl.() -> Unit = {}): ResultActionsDsl { + val requestBuilder = MockMvcRequestBuilders.put(urlTemplate, *vars) + return MockHttpServletRequestDsl(requestBuilder).apply(dsl).perform(this) +} + +/** + * [MockMvc] extension providing access to [MockHttpServletRequestDsl] Kotlin DSL. + * + * @see MockMvcRequestBuilders.put + * @author Sebastien Deleuze + * @since 5.2 + */ +fun MockMvc.put(uri: URI, dsl: MockHttpServletRequestDsl.() -> Unit = {}): ResultActionsDsl { + val requestBuilder = MockMvcRequestBuilders.put(uri) + return MockHttpServletRequestDsl(requestBuilder).apply(dsl).perform(this) +} + +/** + * [MockMvc] extension providing access to [MockHttpServletRequestDsl] Kotlin DSL. + * + * @see MockMvcRequestBuilders.patch + * @author Sebastien Deleuze + * @since 5.2 + */ +fun MockMvc.patch(urlTemplate: String, vararg vars: Any?, dsl: MockHttpServletRequestDsl.() -> Unit = {}): ResultActionsDsl { + val requestBuilder = MockMvcRequestBuilders.patch(urlTemplate, *vars) + return MockHttpServletRequestDsl(requestBuilder).apply(dsl).perform(this) +} + +/** + * [MockMvc] extension providing access to [MockHttpServletRequestDsl] Kotlin DSL. + * + * @see MockMvcRequestBuilders.patch + * @author Sebastien Deleuze + * @since 5.2 + */ +fun MockMvc.patch(uri: URI, dsl: MockHttpServletRequestDsl.() -> Unit = {}): ResultActionsDsl { + val requestBuilder = MockMvcRequestBuilders.patch(uri) + return MockHttpServletRequestDsl(requestBuilder).apply(dsl).perform(this) +} + +/** + * [MockMvc] extension providing access to [MockHttpServletRequestDsl] Kotlin DSL. + * + * @see MockMvcRequestBuilders.delete + * @author Sebastien Deleuze + * @since 5.2 + */ +fun MockMvc.delete(urlTemplate: String, vararg vars: Any?, dsl: MockHttpServletRequestDsl.() -> Unit = {}): ResultActionsDsl { + val requestBuilder = MockMvcRequestBuilders.delete(urlTemplate, *vars) + return MockHttpServletRequestDsl(requestBuilder).apply(dsl).perform(this) +} + +/** + * [MockMvc] extension providing access to [MockHttpServletRequestDsl] Kotlin DSL. + * + * @see MockMvcRequestBuilders.delete + * @author Sebastien Deleuze + * @since 5.2 + */ +fun MockMvc.delete(uri: URI, dsl: MockHttpServletRequestDsl.() -> Unit = {}): ResultActionsDsl { + val requestBuilder = MockMvcRequestBuilders.delete(uri) + return MockHttpServletRequestDsl(requestBuilder).apply(dsl).perform(this) +} + +/** + * [MockMvc] extension providing access to [MockHttpServletRequestDsl] Kotlin DSL. + * + * @see MockMvcRequestBuilders.options + * @author Sebastien Deleuze + * @since 5.2 + */ +fun MockMvc.options(urlTemplate: String, vararg vars: Any?, dsl: MockHttpServletRequestDsl.() -> Unit = {}): ResultActionsDsl { + val requestBuilder = MockMvcRequestBuilders.options(urlTemplate, *vars) + return MockHttpServletRequestDsl(requestBuilder).apply(dsl).perform(this) +} + +/** + * [MockMvc] extension providing access to [MockHttpServletRequestDsl] Kotlin DSL. + * + * @see MockMvcRequestBuilders.options + * @author Sebastien Deleuze + * @since 5.2 + */ +fun MockMvc.options(uri: URI, dsl: MockHttpServletRequestDsl.() -> Unit = {}): ResultActionsDsl { + val requestBuilder = MockMvcRequestBuilders.options(uri) + return MockHttpServletRequestDsl(requestBuilder).apply(dsl).perform(this) +} + +/** + * [MockMvc] extension providing access to [MockHttpServletRequestDsl] Kotlin DSL. + * + * @see MockMvcRequestBuilders.request + * @author Sebastien Deleuze + * @since 5.2 + */ +fun MockMvc.request(method: HttpMethod, urlTemplate: String, vararg vars: Any?, dsl: MockHttpServletRequestDsl.() -> Unit = {}): ResultActionsDsl { + val requestBuilder = MockMvcRequestBuilders.request(method, urlTemplate, *vars) + return MockHttpServletRequestDsl(requestBuilder).apply(dsl).perform(this) +} + +/** + * [MockMvc] extension providing access to [MockHttpServletRequestDsl] Kotlin DSL. + * + * @see MockMvcRequestBuilders.request + * @author Sebastien Deleuze + * @since 5.2 + */ +fun MockMvc.request(method: HttpMethod, uri: URI, dsl: MockHttpServletRequestDsl.() -> Unit = {}): ResultActionsDsl { + val requestBuilder = MockMvcRequestBuilders.request(method, uri) + return MockHttpServletRequestDsl(requestBuilder).apply(dsl).perform(this) +} + +/** + * [MockMvc] extension providing access to [MockMultipartHttpServletRequestDsl] Kotlin DSL. + * + * @see MockMvcRequestBuilders.multipart + * @author Sebastien Deleuze + * @since 5.2 + */ +fun MockMvc.multipart(urlTemplate: String, vararg vars: Any?, dsl: MockMultipartHttpServletRequestDsl.() -> Unit = {}): ResultActionsDsl { + val requestBuilder = MockMvcRequestBuilders.multipart(urlTemplate, *vars) + return MockMultipartHttpServletRequestDsl(requestBuilder).apply(dsl).perform(this) +} + +/** + * [MockMvc] extension providing access to [MockMultipartHttpServletRequestDsl] Kotlin DSL. + * + * @see MockMvcRequestBuilders.multipart + * @author Sebastien Deleuze + * @since 5.2 + */ +fun MockMvc.multipart(uri: URI, dsl: MockMultipartHttpServletRequestDsl.() -> Unit = {}): ResultActionsDsl { + val requestBuilder = MockMvcRequestBuilders.multipart(uri) + return MockMultipartHttpServletRequestDsl(requestBuilder).apply(dsl).perform(this) +} diff --git a/spring-test/src/main/kotlin/org/springframework/test/web/servlet/MockMvcResultHandlersDsl.kt b/spring-test/src/main/kotlin/org/springframework/test/web/servlet/MockMvcResultHandlersDsl.kt new file mode 100644 index 0000000000..bf93617395 --- /dev/null +++ b/spring-test/src/main/kotlin/org/springframework/test/web/servlet/MockMvcResultHandlersDsl.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2019 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 + * + * http://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 + +import org.springframework.test.web.servlet.result.* +import java.io.OutputStream +import java.io.Writer + +/** + * Provide a [MockMvcResultHandlers] Kotlin DSL in order to be able to write idiomatic Kotlin code. + * + * @author Sebastien Deleuze + * @since 5.2 + */ +class MockMvcResultHandlersDsl(private val actions: ResultActions) { + + /** + * @see MockMvcResultHandlers.print + */ + fun print() { + actions.andDo(MockMvcResultHandlers.print()) + } + + /** + * @see MockMvcResultHandlers.print + */ + fun print(stream: OutputStream) { + actions.andDo(MockMvcResultHandlers.print(stream)) + } + + /** + * @see MockMvcResultHandlers.print + */ + fun print(writer: Writer) { + actions.andDo(MockMvcResultHandlers.print(writer)) + } + + /** + * @see MockMvcResultHandlers.log + */ + fun log() { + actions.andDo(MockMvcResultHandlers.log()) + } + + /** + * @see ResultActions.andDo + */ + fun handle(resultHandler: ResultHandler) { + actions.andDo(resultHandler) + } +} 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 new file mode 100644 index 0000000000..5407c77e63 --- /dev/null +++ b/spring-test/src/main/kotlin/org/springframework/test/web/servlet/MockMvcResultMatchersDsl.kt @@ -0,0 +1,149 @@ +/* + * Copyright 2002-2019 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 + * + * http://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 + +import org.hamcrest.Matcher +import org.springframework.test.web.servlet.result.* + +/** + * Provide a [MockMvcResultMatchers] Kotlin DSL in order to be able to write idiomatic Kotlin code. + * + * @author Sebastien Deleuze + * @since 5.2 + */ +class MockMvcResultMatchersDsl(private val actions: ResultActions) { + + /** + * @see MockMvcResultMatchers.request + */ + fun request(matcher: RequestResultMatchers.() -> ResultMatcher) { + actions.andExpect(MockMvcResultMatchers.request().matcher()) + } + + /** + * @see MockMvcResultMatchers.view + */ + fun view(matcher: ViewResultMatchers.() -> ResultMatcher) { + actions.andExpect(MockMvcResultMatchers.view().matcher()) + } + + /** + * @see MockMvcResultMatchers.model + */ + fun model(matcher: ModelResultMatchers.() -> ResultMatcher) { + actions.andExpect(MockMvcResultMatchers.model().matcher()) + } + + /** + * @see MockMvcResultMatchers.flash + */ + fun flash(matcher: FlashAttributeResultMatchers.() -> ResultMatcher) { + actions.andExpect(MockMvcResultMatchers.flash().matcher()) + } + + /** + * @see MockMvcResultMatchers.forwardedUrl + */ + fun forwardedUrl(expectedUrl: String?) { + actions.andExpect(MockMvcResultMatchers.forwardedUrl(expectedUrl)) + } + + /** + * @see MockMvcResultMatchers.forwardedUrlTemplate + */ + fun forwardedUrlTemplate(urlTemplate: String, vararg uriVars: Any) { + actions.andExpect(MockMvcResultMatchers.forwardedUrlTemplate(urlTemplate, *uriVars)) + } + + /** + * @see MockMvcResultMatchers.forwardedUrlPattern + */ + fun forwardedUrlPattern(urlPattern: String) { + actions.andExpect(MockMvcResultMatchers.forwardedUrlPattern(urlPattern)) + } + + /** + * @see MockMvcResultMatchers.redirectedUrl + */ + fun redirectedUrl(expectedUrl: String) { + actions.andExpect(MockMvcResultMatchers.redirectedUrl(expectedUrl)) + } + + /** + * @see MockMvcResultMatchers.redirectedUrlPattern + */ + fun redirectedUrlPattern(redirectedUrlPattern: String) { + actions.andExpect(MockMvcResultMatchers.redirectedUrlPattern(redirectedUrlPattern)) + } + + /** + * @see MockMvcResultMatchers.status + */ + fun status(matcher: StatusResultMatchers.() -> ResultMatcher) { + actions.andExpect(MockMvcResultMatchers.status().matcher()) + } + + /** + * @see MockMvcResultMatchers.header + */ + fun header(matcher: HeaderResultMatchers.() -> ResultMatcher) { + actions.andExpect(MockMvcResultMatchers.header().matcher()) + } + + /** + * @see MockMvcResultMatchers.content + */ + fun content(matcher: ContentResultMatchers.() -> ResultMatcher) { + actions.andExpect(MockMvcResultMatchers.content().matcher()) + } + + /** + * @see MockMvcResultMatchers.jsonPath + */ + fun jsonPath(expression: String, matcher: Matcher) { + actions.andExpect(MockMvcResultMatchers.jsonPath(expression, matcher)) + } + + /** + * @see MockMvcResultMatchers.jsonPath + */ + fun jsonPath(expression: String, vararg args: Any, block: JsonPathResultMatchers.() -> ResultMatcher) { + actions.andExpect(MockMvcResultMatchers.jsonPath(expression, *args).block()) + } + + /** + * @see MockMvcResultMatchers.xpath + */ + fun xpath(expression: String, vararg args: Any, namespaces: Map? = null, xpathInit: XpathResultMatchers.() -> ResultMatcher) { + actions.andExpect(MockMvcResultMatchers.xpath(expression, namespaces, args).xpathInit()) + } + + /** + * @see MockMvcResultMatchers.cookie + */ + fun cookie(cookieInit: CookieResultMatchers.() -> ResultMatcher) { + val cookie = MockMvcResultMatchers.cookie().cookieInit() + actions.andExpect(cookie) + } + + /** + * @see ResultActions.andExpect + */ + fun match(matcher: ResultMatcher) { + actions.andExpect(matcher) + } +} 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 new file mode 100644 index 0000000000..3528391f5a --- /dev/null +++ b/spring-test/src/main/kotlin/org/springframework/test/web/servlet/ResultActionsDsl.kt @@ -0,0 +1,33 @@ +package org.springframework.test.web.servlet + +/** + * Provide a [ResultActions] Kotlin DSL in order to be able to write idiomatic Kotlin code. + * + * @author Sebastien Deleuze + * @since 5.2 + */ +class ResultActionsDsl(private val actions: ResultActions) { + + /** + * Provide access to [MockMvcResultMatchersDsl] Kotlin DSL. + * @see MockMvcResultMatchersDsl.match + */ + infix fun andExpect(dsl: MockMvcResultMatchersDsl.() -> Unit): ResultActionsDsl { + MockMvcResultMatchersDsl(actions).dsl() + return this + } + + /** + * Provide access to [MockMvcResultHandlersDsl] Kotlin DSL. + * @see MockMvcResultHandlersDsl.handle + */ + infix fun andDo(dsl: MockMvcResultHandlersDsl.() -> Unit): ResultActionsDsl { + MockMvcResultHandlersDsl(actions).dsl() + return this + } + + /** + * @see ResultActions.andReturn + */ + fun andReturn() = actions.andReturn() +} 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 new file mode 100644 index 0000000000..a15484408d --- /dev/null +++ b/spring-test/src/test/kotlin/org/springframework/test/web/servlet/MockMvcExtensionsTests.kt @@ -0,0 +1,172 @@ +/* + * Copyright 2002-2019 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 + * + * http://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 + +import org.hamcrest.CoreMatchers +import org.junit.Assert +import org.junit.Test +import org.junit.jupiter.api.assertThrows +import org.springframework.http.HttpMethod +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.test.web.Person +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import org.springframework.web.bind.annotation.* +import java.security.Principal +import java.util.* + +/** + * [MockMvc] DSL tests that verifies builder, actions and expect blocks. + * + * @author Clint Checketts + * @author Petr Balat + * @author Sebastien Deleuze + */ +class MockMvcExtensionsTests { + + private val mockMvc = MockMvcBuilders.standaloneSetup(PersonController()).build() + + @Test + fun request() { + mockMvc.request(HttpMethod.GET, "/person/{name}", "Lee") { + secure = true + accept = MediaType.APPLICATION_JSON + headers { + contentLanguage = Locale.FRANCE + } + principal = Principal { "foo" } + } andExpect { + status { isOk } + content { contentType("application/json;charset=UTF-8") } + jsonPath("$.name") { value("Lee") } + content { json("""{"someBoolean": false}""", false) } + } andDo { + print() + } + } + + @Test + fun `request without MockHttpServletRequestDsl`() { + mockMvc.request(HttpMethod.GET, "/person/{name}", "Lee") andExpect { + status { isOk } + } andDo { + print() + } + } + + @Test + fun `request with custom matcher and handler`() { + var matcherInvoked = false + var handlerInvoked = false + val matcher = ResultMatcher { matcherInvoked = true } + val handler = ResultHandler { handlerInvoked = true } + mockMvc.request(HttpMethod.GET, "/person/{name}", "Lee") andExpect { + status { isOk } + } andExpect { + match(matcher) + } andDo { + handle(handler) + } + Assert.assertTrue(matcherInvoked) + Assert.assertTrue(handlerInvoked) + } + + @Test + fun get() { + mockMvc.get("/person/{name}", "Lee") { + secure = true + accept = MediaType.APPLICATION_JSON + headers { + contentLanguage = Locale.FRANCE + } + principal = Principal { "foo" } + } andExpect { + status { isOk } + content { contentType("application/json;charset=UTF-8") } + jsonPath("$.name") { value("Lee") } + content { json("""{"someBoolean": false}""", false) } + } andDo { + print() + } + } + + @Test + fun post() { + mockMvc.post("/person") { + content = """{ "name": "foo" }""" + headers { + accept = listOf(MediaType.APPLICATION_JSON) + contentType = MediaType.APPLICATION_JSON + } + } andExpect { + status { + isCreated + } + } + } + + @Test + fun `negative assertion tests to verify the matchers throw errors when expected`() { + val name = "Petr" + mockMvc.get("/person/$name") { + accept = MediaType.APPLICATION_JSON + } andExpect { + assertThrows { content { contentType(MediaType.APPLICATION_ATOM_XML) } } + assertThrows { content { string("Wrong") } } + assertThrows { jsonPath("name", CoreMatchers.`is`("Wrong")) } + assertThrows { content { json("""{"name":"wrong"}""") } } + assertThrows { jsonPath("name") { value("wrong") } } + assertThrows { cookie { value("name", "wrong") } } + assertThrows { flash { attribute("name", "wrong") } } + assertThrows { header { stringValues("name", "wrong") } } + assertThrows { model { attributeExists("name", "wrong") } } + assertThrows { redirectedUrl("wrong/Url") } + assertThrows { redirectedUrlPattern("wrong/Url") } + assertThrows { redirectedUrlPattern("wrong/Url") } + assertThrows { status { isAccepted } } + assertThrows { view { name("wrongName") } } + assertThrows { jsonPath("name") { value("wrong") } } + } + } + + @Test + fun `negative assertion tests for xpath`() { + mockMvc.get("/person/Clint") { + accept = MediaType.APPLICATION_XML + } andExpect { + status { isOk } + assertThrows { xpath("//wrong") { nodeCount(1) } } + } andDo { + print() + } + } + + + @RestController + private class PersonController { + + @GetMapping("/person/{name}") + fun get(@PathVariable name: String): Person { + return Person(name) + } + + @Suppress("UNUSED_PARAMETER") + @PostMapping("/person") + @ResponseStatus(HttpStatus.CREATED) + fun post(@RequestBody person: Person) {} + } +} diff --git a/src/docs/asciidoc/languages/kotlin.adoc b/src/docs/asciidoc/languages/kotlin.adoc index 9ec31c0f0c..17f9547e46 100644 --- a/src/docs/asciidoc/languages/kotlin.adoc +++ b/src/docs/asciidoc/languages/kotlin.adoc @@ -326,6 +326,31 @@ class UserHandler(builder: WebClient.Builder) { Read this blog post about https://medium.com/@elizarov/structured-concurrency-722d765aa952[structured concurrency] to understand how to run code concurrently with Coroutines. +=== MockMvc DSL + +A Kotlin DSL is provided via `MockMvc` Kotlin extensions in order to provide a more idiomatic Kotlin API and to allow +better discoverability (no usage of static methods). + +[source,kotlin,indent=0] +---- +val mockMvc: MockMvc = ... +mockMvc.get("/person/{name}", "Lee") { + secure = true + accept = MediaType.APPLICATION_JSON + headers { + contentLanguage = Locale.FRANCE + } + principal = Principal { "foo" } +} andExpect { + status { isOk } + content { contentType("application/json;charset=UTF-8") } + jsonPath("$.name") { value("Lee") } + content { json("""{"someBoolean": false}""", false) } +} andDo { + print() +} +---- + === Kotlin Script Templates As of version 4.3, Spring Framework provides a