Add body methods with Object parameter to WebFlux

The commit deprecates syncBody(Object) in favor of body(Object)
which has the same behavior in ServerResponse, WebClient and
WebTestClient. It also adds body(Object, Class) and
body(Object, ParameterizedTypeReference) methods in order to support
any reactive type that can be adapted to a Publisher via
ReactiveAdapterRegistry. Related BodyInserters#fromProducer
methods are provided as well.

Shadowed Kotlin body<T>() extensions are deprecated in favor of
bodyWithType<T>() ones, including dedicated Publisher<T> and
Flow<T> variants. Coroutines extensions are adapted as well, and
body(Object) can now be used with suspending functions.

Closes gh-23212
This commit is contained in:
Sebastien Deleuze
2019-07-07 21:03:41 +02:00
parent 0fbc9bf461
commit 2b4d6ce354
33 changed files with 781 additions and 270 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* 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.
@@ -261,8 +261,20 @@ class DefaultWebTestClient implements WebTestClient {
}
@Override
public RequestHeadersSpec<?> body(BodyInserter<?, ? super ClientHttpRequest> inserter) {
this.bodySpec.body(inserter);
public RequestHeadersSpec<?> body(Object body) {
this.bodySpec.body(body);
return this;
}
@Override
public RequestHeadersSpec<?> body(Object producer, Class<?> elementClass) {
this.bodySpec.body(producer, elementClass);
return this;
}
@Override
public RequestHeadersSpec<?> body(Object producer, ParameterizedTypeReference<?> elementType) {
this.bodySpec.body(producer, elementType);
return this;
}
@@ -273,11 +285,23 @@ class DefaultWebTestClient implements WebTestClient {
}
@Override
public RequestHeadersSpec<?> syncBody(Object body) {
this.bodySpec.syncBody(body);
public <T, S extends Publisher<T>> RequestHeadersSpec<?> body(S publisher, ParameterizedTypeReference<T> elementType) {
this.bodySpec.body(publisher, elementType);
return this;
}
@Override
public RequestHeadersSpec<?> body(BodyInserter<?, ? super ClientHttpRequest> inserter) {
this.bodySpec.body(inserter);
return this;
}
@Override
@Deprecated
public RequestHeadersSpec<?> syncBody(Object body) {
return body(body);
}
@Override
public ResponseSpec exchange() {
ClientResponse clientResponse = this.bodySpec.exchange().block(getTimeout());

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* 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.
@@ -30,6 +30,7 @@ import org.reactivestreams.Publisher;
import org.springframework.context.ApplicationContext;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
@@ -625,26 +626,7 @@ public interface WebTestClient {
RequestBodySpec contentType(MediaType contentType);
/**
* Set the body of the request to the given {@code BodyInserter}.
* @param inserter the inserter
* @return spec for decoding the response
* @see org.springframework.web.reactive.function.BodyInserters
*/
RequestHeadersSpec<?> body(BodyInserter<?, ? super ClientHttpRequest> inserter);
/**
* Set the body of the request to the given asynchronous {@code Publisher}.
* @param publisher the request body data
* @param elementClass the class of elements contained in the publisher
* @param <T> the type of the elements contained in the publisher
* @param <S> the type of the {@code Publisher}
* @return spec for decoding the response
*/
<T, S extends Publisher<T>> RequestHeadersSpec<?> body(S publisher, Class<T> elementClass);
/**
* Set the body of the request to the given synchronous {@code Object} and
* perform the request.
* Set the body of the request to the given {@code Object} and perform the request.
* <p>This method is a convenient shortcut for:
* <pre class="code">
* .body(BodyInserters.fromObject(object))
@@ -657,8 +639,83 @@ public interface WebTestClient {
* part with body and headers. The {@code MultiValueMap} can be built
* conveniently using
* @param body the {@code Object} to write to the request
* @return a {@code Mono} with the response
* @return spec for decoding the response
* @since 5.2
*/
RequestHeadersSpec<?> body(Object body);
/**
* Set the body of the request to the given producer.
* @param producer the producer to write to the request. This must be a
* {@link Publisher} or another producer adaptable to a
* {@code Publisher} via {@link ReactiveAdapterRegistry}
* @param elementClass the class of elements contained in the producer
* @return spec for decoding the response
* @since 5.2
*/
RequestHeadersSpec<?> body(Object producer, Class<?> elementClass);
/**
* Set the body of the request to the given producer.
* @param producer the producer to write to the request. This must be a
* {@link Publisher} or another producer adaptable to a
* {@code Publisher} via {@link ReactiveAdapterRegistry}
* @param elementType the type reference of elements contained in the producer
* @return spec for decoding the response
* @since 5.2
*/
RequestHeadersSpec<?> body(Object producer, ParameterizedTypeReference<?> elementType);
/**
* Set the body of the request to the given asynchronous {@code Publisher}.
* @param publisher the request body data
* @param elementClass the class of elements contained in the publisher
* @param <T> the type of the elements contained in the publisher
* @param <S> the type of the {@code Publisher}
* @return spec for decoding the response
*/
<T, S extends Publisher<T>> RequestHeadersSpec<?> body(S publisher, Class<T> elementClass);
/**
* Set the body of the request to the given asynchronous {@code Publisher}.
* @param publisher the request body data
* @param elementType the type reference of elements contained in the publisher
* @param <T> the type of the elements contained in the publisher
* @param <S> the type of the {@code Publisher}
* @return spec for decoding the response
* @since 5.2
*/
<T, S extends Publisher<T>> RequestHeadersSpec<?> body(S publisher, ParameterizedTypeReference<T> elementType);
/**
* Set the body of the request to the given {@code BodyInserter}.
* @param inserter the inserter
* @return spec for decoding the response
* @see org.springframework.web.reactive.function.BodyInserters
*/
RequestHeadersSpec<?> body(BodyInserter<?, ? super ClientHttpRequest> inserter);
/**
* Set the body of the request to the given {@code Object} and perform the request.
* <p>This method is a convenient shortcut for:
* <pre class="code">
* .body(BodyInserters.fromObject(object))
* </pre>
* <p>The body can be a
* {@link org.springframework.util.MultiValueMap MultiValueMap} to create
* a multipart request. The values in the {@code MultiValueMap} can be
* any Object representing the body of the part, or an
* {@link org.springframework.http.HttpEntity HttpEntity} representing a
* part with body and headers. The {@code MultiValueMap} can be built
* conveniently using
* @param body the {@code Object} to write to the request
* @return spec for decoding the response
* @throws IllegalArgumentException if {@code body} is a {@link Publisher} or an
* instance of a type supported by {@link ReactiveAdapterRegistry#getSharedInstance()},
* for which {@link #body(Publisher, Class)} or {@link #body(Object, Class)} should be used.
* @deprecated as of Spring Framework 5.2 in favor of {@link #body(Object)}
*/
@Deprecated
RequestHeadersSpec<?> syncBody(Object body);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* 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.
@@ -16,7 +16,10 @@
package org.springframework.test.web.reactive.server
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import org.reactivestreams.Publisher
import org.springframework.core.ParameterizedTypeReference
import org.springframework.test.util.AssertionErrors.assertEquals
import org.springframework.test.web.reactive.server.WebTestClient.*
@@ -27,8 +30,49 @@ import org.springframework.test.web.reactive.server.WebTestClient.*
* @author Sebastien Deleuze
* @since 5.0
*/
@Deprecated("Use 'bodyWithType' instead.", replaceWith = ReplaceWith("bodyWithType(publisher)"))
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
inline fun <reified T : Any, S : Publisher<T>> RequestBodySpec.body(publisher: S): RequestHeadersSpec<*>
= body(publisher, T::class.java)
= body(publisher, object : ParameterizedTypeReference<T>() {})
/**
* Extension for [RequestBodySpec.body] providing a `bodyWithType<T>(Any)` variant
* leveraging Kotlin reified type parameters. This extension is not subject to type
* erasure and retains actual generic type arguments.
* @param producer the producer to write to the request. This must be a
* [Publisher] or another producer adaptable to a
* [Publisher] via [org.springframework.core.ReactiveAdapterRegistry]
* @param <T> the type of the elements contained in the producer
* @author Sebastien Deleuze
* @since 5.2
*/
inline fun <reified T : Any> RequestBodySpec.bodyWithType(producer: Any): RequestHeadersSpec<*>
= body(producer, object : ParameterizedTypeReference<T>() {})
/**
* Extension for [RequestBodySpec.body] providing a `bodyWithType(Publisher<T>)` variant
* leveraging Kotlin reified type parameters. This extension is not subject to type
* erasure and retains actual generic type arguments.
* @param publisher the [Publisher] to write to the request
* @param <T> the type of the elements contained in the publisher
* @author Sebastien Deleuze
* @since 5.2
*/
inline fun <reified T : Any> RequestBodySpec.bodyWithType(publisher: Publisher<T>): RequestHeadersSpec<*> =
body(publisher, object : ParameterizedTypeReference<T>() {})
/**
* Extension for [RequestBodySpec.body] providing a `bodyWithType(Flow<T>)` variant
* leveraging Kotlin reified type parameters. This extension is not subject to type
* erasure and retains actual generic type arguments.
* @param flow the [Flow] to write to the request
* @param <T> the type of the elements contained in the publisher
* @author Sebastien Deleuze
* @since 5.2
*/
@FlowPreview
inline fun <reified T : Any> RequestBodySpec.bodyWithType(flow: Flow<T>): RequestHeadersSpec<*> =
body(flow, object : ParameterizedTypeReference<T>() {})
/**
* Extension for [ResponseSpec.expectBody] providing an `expectBody<Foo>()` variant and
@@ -44,13 +88,11 @@ inline fun <reified B : Any> ResponseSpec.expectBody(): KotlinBodySpec<B> =
object : KotlinBodySpec<B> {
override fun isEqualTo(expected: B): KotlinBodySpec<B> = it
.assertWithDiagnostics({ assertEquals("Response body", expected, it.responseBody) })
.let { this }
.assertWithDiagnostics { assertEquals("Response body", expected, it.responseBody) }
.let { this }
override fun consumeWith(consumer: (EntityExchangeResult<B>) -> Unit): KotlinBodySpec<B> =
it
.assertWithDiagnostics({ consumer.invoke(it) })
.let { this }
it.assertWithDiagnostics { consumer.invoke(it) }.let { this }
override fun returnResult(): EntityExchangeResult<B> = it
}
@@ -88,7 +130,7 @@ interface KotlinBodySpec<B> {
* @since 5.0
*/
inline fun <reified E : Any> ResponseSpec.expectBodyList(): ListBodySpec<E> =
expectBodyList(E::class.java)
expectBodyList(object : ParameterizedTypeReference<E>() {})
/**
* Extension for [ResponseSpec.returnResult] providing a `returnResult<Foo>()` variant.
@@ -98,4 +140,4 @@ inline fun <reified E : Any> ResponseSpec.expectBodyList(): ListBodySpec<E> =
*/
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
inline fun <reified T : Any> ResponseSpec.returnResult(): FluxExchangeResult<T> =
returnResult(T::class.java)
returnResult(object : ParameterizedTypeReference<T>() {})

View File

@@ -61,7 +61,7 @@ public class ApplicationContextSpecTests {
.GET("/sessionClassName", request ->
request.session().flatMap(session -> {
String className = session.getClass().getSimpleName();
return ServerResponse.ok().syncBody(className);
return ServerResponse.ok().body(className);
}))
.build();
}

View File

@@ -63,7 +63,7 @@ public class ErrorTests {
EntityExchangeResult<Void> result = this.client.post()
.uri("/post")
.contentType(MediaType.APPLICATION_JSON)
.syncBody(new Person("Dan"))
.body(new Person("Dan"))
.exchange()
.expectStatus().isBadRequest()
.expectBody().isEmpty();

View File

@@ -82,7 +82,7 @@ public class JsonContentTests {
public void postJsonContent() {
this.client.post().uri("/persons")
.contentType(MediaType.APPLICATION_JSON)
.syncBody("{\"name\":\"John\"}")
.body("{\"name\":\"John\"}")
.exchange()
.expectStatus().isCreated()
.expectBody().isEmpty();

View File

@@ -145,7 +145,7 @@ public class ResponseEntityTests {
@Test
public void postEntity() {
this.client.post()
.syncBody(new Person("John"))
.body(new Person("John"))
.exchange()
.expectStatus().isCreated()
.expectHeader().valueEquals("location", "/persons/John")

View File

@@ -116,7 +116,7 @@ public class XmlContentTests {
this.client.post().uri("/persons")
.contentType(MediaType.APPLICATION_XML)
.syncBody(content)
.body(content)
.exchange()
.expectStatus().isCreated()
.expectHeader().valueEquals(HttpHeaders.LOCATION, "/persons/John")

View File

@@ -45,7 +45,7 @@ public class HttpServerTests {
@Before
public void start() throws Exception {
HttpHandler httpHandler = RouterFunctions.toHttpHandler(
route(GET("/test"), request -> ServerResponse.ok().syncBody("It works!")));
route(GET("/test"), request -> ServerResponse.ok().body("It works!")));
this.server = new ReactorHttpServer();
this.server.setHandler(httpHandler);

View File

@@ -41,7 +41,7 @@ public class RouterFunctionTests {
public void setUp() throws Exception {
RouterFunction<?> route = route(GET("/test"), request ->
ServerResponse.ok().syncBody("It works!"));
ServerResponse.ok().body("It works!"));
this.testClient = WebTestClient.bindToRouterFunction(route).build();
}

View File

@@ -18,10 +18,14 @@ package org.springframework.test.web.reactive.server
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import org.junit.Assert.assertEquals
import org.junit.Test
import org.reactivestreams.Publisher
import org.springframework.core.ParameterizedTypeReference
import org.springframework.web.reactive.function.server.router
import java.util.concurrent.CompletableFuture
/**
* Mock object based tests for [WebTestClient] Kotlin extensions
@@ -30,16 +34,31 @@ import org.springframework.web.reactive.function.server.router
*/
class WebTestClientExtensionsTests {
val requestBodySpec = mockk<WebTestClient.RequestBodySpec>(relaxed = true)
private val requestBodySpec = mockk<WebTestClient.RequestBodySpec>(relaxed = true)
val responseSpec = mockk<WebTestClient.ResponseSpec>(relaxed = true)
private val responseSpec = mockk<WebTestClient.ResponseSpec>(relaxed = true)
@Test
fun `RequestBodySpec#body with Publisher and reified type parameters`() {
fun `RequestBodySpec#bodyWithType with Publisher and reified type parameters`() {
val body = mockk<Publisher<Foo>>()
requestBodySpec.body(body)
verify { requestBodySpec.body(body, Foo::class.java) }
requestBodySpec.bodyWithType(body)
verify { requestBodySpec.body(body, object : ParameterizedTypeReference<Foo>() {}) }
}
@Test
@FlowPreview
fun `RequestBodySpec#bodyWithType with Flow and reified type parameters`() {
val body = mockk<Flow<Foo>>()
requestBodySpec.bodyWithType(body)
verify { requestBodySpec.body(body, object : ParameterizedTypeReference<Foo>() {}) }
}
@Test
fun `RequestBodySpec#bodyWithType with CompletableFuture and reified type parameters`() {
val body = mockk<CompletableFuture<Foo>>()
requestBodySpec.bodyWithType<Foo>(body)
verify { requestBodySpec.body(body, object : ParameterizedTypeReference<Foo>() {}) }
}
@Test
@@ -51,7 +70,7 @@ class WebTestClientExtensionsTests {
@Test
fun `KotlinBodySpec#isEqualTo`() {
WebTestClient
.bindToRouterFunction( router { GET("/") { ok().syncBody("foo") } } )
.bindToRouterFunction( router { GET("/") { ok().body("foo") } } )
.build()
.get().uri("/").exchange().expectBody<String>().isEqualTo("foo")
}
@@ -59,7 +78,7 @@ class WebTestClientExtensionsTests {
@Test
fun `KotlinBodySpec#consumeWith`() {
WebTestClient
.bindToRouterFunction( router { GET("/") { ok().syncBody("foo") } } )
.bindToRouterFunction( router { GET("/") { ok().body("foo") } } )
.build()
.get().uri("/").exchange().expectBody<String>().consumeWith { assertEquals("foo", it.responseBody) }
}
@@ -67,7 +86,7 @@ class WebTestClientExtensionsTests {
@Test
fun `KotlinBodySpec#returnResult`() {
WebTestClient
.bindToRouterFunction( router { GET("/") { ok().syncBody("foo") } } )
.bindToRouterFunction( router { GET("/") { ok().body("foo") } } )
.build()
.get().uri("/").exchange().expectBody<String>().returnResult().apply { assertEquals("foo", responseBody) }
}
@@ -75,13 +94,13 @@ class WebTestClientExtensionsTests {
@Test
fun `ResponseSpec#expectBodyList with reified type parameters`() {
responseSpec.expectBodyList<Foo>()
verify { responseSpec.expectBodyList(Foo::class.java) }
verify { responseSpec.expectBodyList(object : ParameterizedTypeReference<Foo>() {}) }
}
@Test
fun `ResponseSpec#returnResult with reified type parameters`() {
responseSpec.returnResult<Foo>()
verify { responseSpec.returnResult(Foo::class.java) }
verify { responseSpec.returnResult(object : ParameterizedTypeReference<Foo>() {}) }
}
class Foo