diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java index e033410cd70..0659241cae2 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java @@ -261,18 +261,6 @@ class DefaultWebTestClient implements WebTestClient { return this; } - @Override - public RequestBodySpec attribute(String name, Object value) { - this.attributes.put(name, value); - return this; - } - - @Override - public RequestBodySpec attributes(Consumer> attributesConsumer) { - attributesConsumer.accept(this.attributes); - return this; - } - @Override public RequestBodySpec accept(MediaType... acceptableMediaTypes) { getHeaders().setAccept(Arrays.asList(acceptableMediaTypes)); @@ -321,6 +309,18 @@ class DefaultWebTestClient implements WebTestClient { return this; } + @Override + public RequestBodySpec attribute(String name, Object value) { + this.attributes.put(name, value); + return this; + } + + @Override + public RequestBodySpec attributes(Consumer> attributesConsumer) { + attributesConsumer.accept(this.attributes); + return this; + } + @Override public RequestBodySpec apiVersion(Object version) { this.apiVersion = version; diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java index 16127ae8d13..210146bc83f 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java @@ -820,6 +820,7 @@ public interface WebTestClient { interface RequestHeadersUriSpec> extends UriSpec, RequestHeadersSpec { } + /** * Specification for providing the body and the URI of a request. */ @@ -932,7 +933,6 @@ public interface WebTestClient { @FunctionalInterface interface ResponseSpecConsumer extends Consumer { } - } @@ -1014,7 +1014,6 @@ public interface WebTestClient { * Spec for expectations on the response body content. */ interface BodyContentSpec { - /** * Assert the response body is empty and return the exchange result. */ diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/client/CookieAssertions.java b/spring-test/src/main/java/org/springframework/test/web/servlet/client/CookieAssertions.java index 8ca598d30b7..b4f8cced077 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/client/CookieAssertions.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/client/CookieAssertions.java @@ -24,6 +24,7 @@ import org.springframework.util.MultiValueMap; * Assertions on cookies of the response. * * @author Rob Worsnop + * @since 7.0 */ public class CookieAssertions extends AbstractCookieAssertions { diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/client/DefaultRestTestClient.java b/spring-test/src/main/java/org/springframework/test/web/servlet/client/DefaultRestTestClient.java index 10bff023a62..d831a68af03 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/client/DefaultRestTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/client/DefaultRestTestClient.java @@ -50,6 +50,8 @@ import org.springframework.web.util.UriBuilder; * Default implementation of {@link RestTestClient}. * * @author Rob Worsnop + * @author Rossen Stoyanchev + * @since 7.0 */ class DefaultRestTestClient implements RestTestClient { @@ -59,11 +61,13 @@ class DefaultRestTestClient implements RestTestClient { private final RestClient.Builder restClientBuilder; + DefaultRestTestClient(RestClient.Builder restClientBuilder) { this.restClient = restClientBuilder.build(); this.restClientBuilder = restClientBuilder; } + @Override public RequestHeadersUriSpec get() { return methodInternal(HttpMethod.GET); @@ -104,74 +108,75 @@ class DefaultRestTestClient implements RestTestClient { return methodInternal(method); } + private RequestBodyUriSpec methodInternal(HttpMethod httpMethod) { + return new DefaultRequestBodyUriSpec(this.restClient.method(httpMethod)); + } + @Override public > Builder mutate() { return new DefaultRestTestClientBuilder<>(this.restClientBuilder); } - private RequestBodyUriSpec methodInternal(HttpMethod httpMethod) { - return new DefaultRequestBodyUriSpec(this.restClient.method(httpMethod)); - } - private class DefaultRequestBodyUriSpec implements RequestBodyUriSpec { private final RestClient.RequestBodyUriSpec requestHeadersUriSpec; + private RestClient.RequestBodySpec requestBodySpec; - private final String requestId; + private final String requestId; - public DefaultRequestBodyUriSpec(RestClient.RequestBodyUriSpec spec) { + DefaultRequestBodyUriSpec(RestClient.RequestBodyUriSpec spec) { this.requestHeadersUriSpec = spec; this.requestBodySpec = spec; this.requestId = String.valueOf(requestIndex.incrementAndGet()); } @Override - public RequestBodySpec accept(MediaType... acceptableMediaTypes) { - this.requestBodySpec = this.requestHeadersUriSpec.accept(acceptableMediaTypes); + public RequestBodySpec uri(String uriTemplate, Object... uriVariables) { + this.requestBodySpec = this.requestHeadersUriSpec.uri(uriTemplate, uriVariables); return this; } @Override - public RequestBodySpec uri(URI uri) { - this.requestBodySpec = this.requestHeadersUriSpec.uri(uri); + public RequestBodySpec uri(String uri, Map uriVariables) { + this.requestBodySpec = this.requestHeadersUriSpec.uri(uri, uriVariables); return this; } @Override - public RequestBodySpec uri(String uriTemplate, Object... uriVariables) { - this.requestBodySpec = this.requestHeadersUriSpec.uri(uriTemplate, uriVariables); + public RequestBodySpec uri(Function uriFunction) { + this.requestBodySpec = this.requestHeadersUriSpec.uri(uriFunction); return this; } @Override - public RequestBodySpec uri(String uri, Map uriVariables) { - this.requestBodySpec = this.requestHeadersUriSpec.uri(uri, uriVariables); + public RequestBodySpec uri(URI uri) { + this.requestBodySpec = this.requestHeadersUriSpec.uri(uri); return this; } @Override - public RequestBodySpec uri(Function uriFunction) { - this.requestBodySpec = this.requestHeadersUriSpec.uri(uriFunction); + public RequestBodySpec header(String headerName, String... headerValues) { + this.requestBodySpec = this.requestHeadersUriSpec.header(headerName, headerValues); return this; } @Override - public RequestBodySpec cookie(String name, String value) { - this.requestBodySpec = this.requestHeadersUriSpec.cookie(name, value); + public RequestBodySpec headers(Consumer headersConsumer) { + this.requestBodySpec = this.requestHeadersUriSpec.headers(headersConsumer); return this; } @Override - public RequestBodySpec cookies(Consumer> cookiesConsumer) { - this.requestBodySpec = this.requestHeadersUriSpec.cookies(cookiesConsumer); + public RequestBodySpec accept(MediaType... acceptableMediaTypes) { + this.requestBodySpec = this.requestHeadersUriSpec.accept(acceptableMediaTypes); return this; } @Override - public RequestBodySpec header(String headerName, String... headerValues) { - this.requestBodySpec = this.requestHeadersUriSpec.header(headerName, headerValues); + public RequestBodySpec acceptCharset(Charset... acceptableCharsets) { + this.requestBodySpec = this.requestHeadersUriSpec.acceptCharset(acceptableCharsets); return this; } @@ -182,14 +187,14 @@ class DefaultRestTestClient implements RestTestClient { } @Override - public RequestHeadersSpec body(Object body) { - this.requestHeadersUriSpec.body(body); + public RequestBodySpec cookie(String name, String value) { + this.requestBodySpec = this.requestHeadersUriSpec.cookie(name, value); return this; } @Override - public RequestBodySpec acceptCharset(Charset... acceptableCharsets) { - this.requestBodySpec = this.requestHeadersUriSpec.acceptCharset(acceptableCharsets); + public RequestBodySpec cookies(Consumer> cookiesConsumer) { + this.requestBodySpec = this.requestHeadersUriSpec.cookies(cookiesConsumer); return this; } @@ -205,12 +210,6 @@ class DefaultRestTestClient implements RestTestClient { return this; } - @Override - public RequestBodySpec headers(Consumer headersConsumer) { - this.requestBodySpec = this.requestHeadersUriSpec.headers(headersConsumer); - return this; - } - @Override public RequestBodySpec attribute(String name, Object value) { this.requestBodySpec = this.requestHeadersUriSpec.attribute(name, value); @@ -223,6 +222,12 @@ class DefaultRestTestClient implements RestTestClient { return this; } + @Override + public RequestHeadersSpec body(Object body) { + this.requestHeadersUriSpec.body(body); + return this; + } + @Override public ResponseSpec exchange() { this.requestBodySpec = this.requestBodySpec.header(RESTTESTCLIENT_REQUEST_ID, this.requestId); @@ -233,11 +238,12 @@ class DefaultRestTestClient implements RestTestClient { } } + private static class DefaultResponseSpec implements ResponseSpec { private final ExchangeResult exchangeResult; - public DefaultResponseSpec(ExchangeResult exchangeResult) { + DefaultResponseSpec(ExchangeResult exchangeResult) { this.exchangeResult = exchangeResult; } @@ -247,9 +253,13 @@ class DefaultRestTestClient implements RestTestClient { } @Override - public BodyContentSpec expectBody() { - byte[] body = this.exchangeResult.getBody(byte[].class); - return new DefaultBodyContentSpec( new EntityExchangeResult<>(this.exchangeResult, body)); + public HeaderAssertions expectHeader() { + return new HeaderAssertions(this.exchangeResult, this); + } + + @Override + public CookieAssertions expectCookie() { + return new CookieAssertions(this.exchangeResult, this); } @Override @@ -265,13 +275,19 @@ class DefaultRestTestClient implements RestTestClient { } @Override - public CookieAssertions expectCookie() { - return new CookieAssertions(this.exchangeResult, this); + public BodyContentSpec expectBody() { + byte[] body = this.exchangeResult.getBody(byte[].class); + return new DefaultBodyContentSpec( new EntityExchangeResult<>(this.exchangeResult, body)); } @Override - public HeaderAssertions expectHeader() { - return new HeaderAssertions(this.exchangeResult, this); + public EntityExchangeResult returnResult(Class elementClass) { + return new EntityExchangeResult<>(this.exchangeResult, this.exchangeResult.getBody(elementClass)); + } + + @Override + public EntityExchangeResult returnResult(ParameterizedTypeReference elementTypeRef) { + return new EntityExchangeResult<>(this.exchangeResult, this.exchangeResult.getBody(elementTypeRef)); } @Override @@ -295,22 +311,63 @@ class DefaultRestTestClient implements RestTestClient { } return this; } + } + + + private static class DefaultBodySpec> implements BodySpec { + + private final EntityExchangeResult result; + + DefaultBodySpec(@Nullable EntityExchangeResult result) { + this.result = Objects.requireNonNull(result, "exchangeResult must be non-null"); + } @Override - public EntityExchangeResult returnResult(Class elementClass) { - return new EntityExchangeResult<>(this.exchangeResult, this.exchangeResult.getBody(elementClass)); + public T isEqualTo(B expected) { + this.result.assertWithDiagnostics(() -> + AssertionErrors.assertEquals("Response body", expected, this.result.getResponseBody())); + return self(); } @Override - public EntityExchangeResult returnResult(ParameterizedTypeReference elementTypeRef) { - return new EntityExchangeResult<>(this.exchangeResult, this.exchangeResult.getBody(elementTypeRef)); + @SuppressWarnings("NullAway") // https://github.com/uber/NullAway/issues/1129 + public T value(Function bodyMapper, Matcher matcher) { + this.result.assertWithDiagnostics(() -> { + B body = this.result.getResponseBody(); + MatcherAssert.assertThat(bodyMapper.apply(body), matcher); + }); + return self(); + } + + @Override + public T value(Consumer consumer) { + this.result.assertWithDiagnostics(() -> consumer.accept(this.result.getResponseBody())); + return self(); + } + + @Override + public T consumeWith(Consumer> consumer) { + this.result.assertWithDiagnostics(() -> consumer.accept(this.result)); + return self(); + } + + @SuppressWarnings("unchecked") + private T self() { + return (T) this; + } + + @Override + public EntityExchangeResult returnResult() { + return this.result; } } + private static class DefaultBodyContentSpec implements BodyContentSpec { + private final EntityExchangeResult result; - public DefaultBodyContentSpec(EntityExchangeResult result) { + DefaultBodyContentSpec(EntityExchangeResult result) { this.result = result; } @@ -374,56 +431,14 @@ class DefaultRestTestClient implements RestTestClient { } @Override - public EntityExchangeResult returnResult() { - return this.result; - } - } - - private static class DefaultBodySpec> implements BodySpec { - - private final EntityExchangeResult result; - - public DefaultBodySpec(@Nullable EntityExchangeResult result) { - this.result = Objects.requireNonNull(result, "exchangeResult must be non-null"); + public BodyContentSpec consumeWith(Consumer> consumer) { + this.result.assertWithDiagnostics(() -> consumer.accept(this.result)); + return this; } @Override - public EntityExchangeResult returnResult() { + public EntityExchangeResult returnResult() { return this.result; } - - @Override - public T isEqualTo(B expected) { - this.result.assertWithDiagnostics(() -> - AssertionErrors.assertEquals("Response body", expected, this.result.getResponseBody())); - return self(); - } - - @Override - @SuppressWarnings("NullAway") // https://github.com/uber/NullAway/issues/1129 - public T value(Function bodyMapper, Matcher matcher) { - this.result.assertWithDiagnostics(() -> { - B body = this.result.getResponseBody(); - MatcherAssert.assertThat(bodyMapper.apply(body), matcher); - }); - return self(); - } - - @Override - public T value(Consumer consumer) { - this.result.assertWithDiagnostics(() -> consumer.accept(this.result.getResponseBody())); - return self(); - } - - @Override - public T consumeWith(Consumer> consumer) { - this.result.assertWithDiagnostics(() -> consumer.accept(this.result)); - return self(); - } - - @SuppressWarnings("unchecked") - private T self() { - return (T) this; - } } } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/client/DefaultRestTestClientBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/client/DefaultRestTestClientBuilder.java index dcd05e779b4..4e4b722e30a 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/client/DefaultRestTestClientBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/client/DefaultRestTestClientBuilder.java @@ -25,13 +25,17 @@ import org.springframework.web.util.UriBuilderFactory; /** * Default implementation of {@link RestTestClient.Builder}. + * * @author Rob Worsnop + * @author Rossen Stoyanchev * @param the type of the builder + * @since 7.0 */ class DefaultRestTestClientBuilder> implements RestTestClient.Builder { protected final RestClient.Builder restClientBuilder; + DefaultRestTestClientBuilder() { this.restClientBuilder = RestClient.builder(); } @@ -40,45 +44,46 @@ class DefaultRestTestClientBuilder> implemen this.restClientBuilder = restClientBuilder; } + @Override - public RestTestClient.Builder apply(Consumer> builderConsumer) { - builderConsumer.accept(this); + public RestTestClient.Builder baseUrl(String baseUrl) { + this.restClientBuilder.baseUrl(baseUrl); return this; } @Override - public RestTestClient.Builder baseUrl(String baseUrl) { - this.restClientBuilder.baseUrl(baseUrl); + public RestTestClient.Builder uriBuilderFactory(UriBuilderFactory uriFactory) { + this.restClientBuilder.uriBuilderFactory(uriFactory); return this; } @Override - public RestTestClient.Builder defaultCookie(String cookieName, String... cookieValues) { - this.restClientBuilder.defaultCookie(cookieName, cookieValues); + public RestTestClient.Builder defaultHeader(String headerName, String... headerValues) { + this.restClientBuilder.defaultHeader(headerName, headerValues); return this; } @Override - public RestTestClient.Builder defaultCookies(Consumer> cookiesConsumer) { - this.restClientBuilder.defaultCookies(cookiesConsumer); + public RestTestClient.Builder defaultHeaders(Consumer headersConsumer) { + this.restClientBuilder.defaultHeaders(headersConsumer); return this; } @Override - public RestTestClient.Builder defaultHeader(String headerName, String... headerValues) { - this.restClientBuilder.defaultHeader(headerName, headerValues); + public RestTestClient.Builder defaultCookie(String cookieName, String... cookieValues) { + this.restClientBuilder.defaultCookie(cookieName, cookieValues); return this; } @Override - public RestTestClient.Builder defaultHeaders(Consumer headersConsumer) { - this.restClientBuilder.defaultHeaders(headersConsumer); + public RestTestClient.Builder defaultCookies(Consumer> cookiesConsumer) { + this.restClientBuilder.defaultCookies(cookiesConsumer); return this; } @Override - public RestTestClient.Builder uriBuilderFactory(UriBuilderFactory uriFactory) { - this.restClientBuilder.uriBuilderFactory(uriFactory); + public RestTestClient.Builder apply(Consumer> builderConsumer) { + builderConsumer.accept(this); return this; } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/client/HeaderAssertions.java b/spring-test/src/main/java/org/springframework/test/web/servlet/client/HeaderAssertions.java index 9429ae85b36..89e557b93e5 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/client/HeaderAssertions.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/client/HeaderAssertions.java @@ -23,6 +23,7 @@ import org.springframework.test.web.support.AbstractHeaderAssertions; * Assertions on headers of the response. * * @author Rob Worsnop + * @since 7.0 * @see RestTestClient.ResponseSpec#expectHeader() */ public class HeaderAssertions extends AbstractHeaderAssertions { diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/client/JsonPathAssertions.java b/spring-test/src/main/java/org/springframework/test/web/servlet/client/JsonPathAssertions.java index cb487bdd4cc..b5eda5ba59a 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/client/JsonPathAssertions.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/client/JsonPathAssertions.java @@ -26,7 +26,7 @@ import org.springframework.test.web.support.AbstractJsonPathAssertions; * JsonPath assertions. * * @author Rob Worsnop - * + * @since 7.0 * @see https://github.com/jayway/JsonPath * @see JsonPathExpectationsHelper */ diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/client/RestTestClient.java b/spring-test/src/main/java/org/springframework/test/web/servlet/client/RestTestClient.java index 23314716fe6..4dcd74026f0 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/client/RestTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/client/RestTestClient.java @@ -63,6 +63,7 @@ public interface RestTestClient { */ String RESTTESTCLIENT_REQUEST_ID = "RestTestClient-Request-Id"; + /** * Prepare an HTTP GET request. * @return a spec for specifying the target URL @@ -111,11 +112,13 @@ public interface RestTestClient { */ RequestBodyUriSpec method(HttpMethod method); + /** * Return a builder to mutate properties of this test client. */ > Builder mutate(); + /** * Begin creating a {@link RestTestClient} by providing the {@code @Controller} * instance(s) to handle requests with. @@ -184,239 +187,78 @@ public interface RestTestClient { return new DefaultRestTestClientBuilder<>(RestClient.builder().requestFactory(requestFactory)); } - /** - * Specification for providing request headers and the URI of a request. - * - * @param a self reference to the spec type - */ - interface RequestHeadersUriSpec> extends UriSpec, RequestHeadersSpec { - } - - /** - * Specification for providing the body and the URI of a request. - */ - interface RequestBodyUriSpec extends RequestBodySpec, RequestHeadersUriSpec { - } - - /** - * Chained API for applying assertions to a response. - */ - interface ResponseSpec { - /** - * Assertions on the response status. - */ - StatusAssertions expectStatus(); - /** - * Consume and decode the response body to {@code byte[]} and then apply - * assertions on the raw content (for example, isEmpty, JSONPath, etc.). - */ - BodyContentSpec expectBody(); + interface Builder> { /** - * Consume and decode the response body to a single object of type - * {@code } and then apply assertions. - * @param bodyType the expected body type + * Configure a base URI as described in + * {@link RestClient#create(String) + * WebClient.create(String)}. */ - BodySpec expectBody(Class bodyType); + Builder baseUrl(String baseUrl); /** - * Alternative to {@link #expectBody(Class)} that accepts information - * about a target type with generics. + * Provide a pre-configured {@link UriBuilderFactory} instance as an + * alternative to and effectively overriding {@link #baseUrl(String)}. */ - BodySpec expectBody(ParameterizedTypeReference bodyType); + Builder uriBuilderFactory(UriBuilderFactory uriBuilderFactory); /** - * Assertions on the cookies of the response. + * Add the given header to all requests that haven't added it. + * @param headerName the header name + * @param headerValues the header values */ - CookieAssertions expectCookie(); + Builder defaultHeader(String headerName, String... headerValues); /** - * Assertions on the headers of the response. + * Manipulate the default headers with the given consumer. The + * headers provided to the consumer are "live", so that the consumer can be used to + * {@linkplain HttpHeaders#set(String, String) overwrite} existing header values, + * {@linkplain HttpHeaders#remove(String) remove} values, or use any of the other + * {@link HttpHeaders} methods. + * @param headersConsumer a function that consumes the {@code HttpHeaders} + * @return this builder */ - HeaderAssertions expectHeader(); + Builder defaultHeaders(Consumer headersConsumer); /** - * Apply multiple assertions to a response with the given - * {@linkplain RestTestClient.ResponseSpec.ResponseSpecConsumer consumers}, with the guarantee that - * all assertions will be applied even if one or more assertions fails - * with an exception. - *

If a single {@link Error} or {@link RuntimeException} is thrown, - * it will be rethrown. - *

If multiple exceptions are thrown, this method will throw an - * {@link AssertionError} whose error message is a summary of all the - * exceptions. In addition, each exception will be added as a - * {@linkplain Throwable#addSuppressed(Throwable) suppressed exception} to - * the {@code AssertionError}. - *

This feature is similar to the {@code SoftAssertions} support in - * AssertJ and the {@code assertAll()} support in JUnit Jupiter. - * - *

Example

- *
-		 * restTestClient.get().uri("/hello").exchange()
-		 *     .expectAll(
-		 *         responseSpec -> responseSpec.expectStatus().isOk(),
-		 *         responseSpec -> responseSpec.expectBody(String.class).isEqualTo("Hello, World!")
-		 *     );
-		 * 
- * @param consumers the list of {@code ResponseSpec} consumers + * Add the given cookie to all requests. + * @param cookieName the cookie name + * @param cookieValues the cookie values */ - ResponseSpec expectAll(ResponseSpecConsumer... consumers); + Builder defaultCookie(String cookieName, String... cookieValues); /** - * Exit the chained flow in order to consume the response body - * externally. + * Manipulate the default cookies with the given consumer. The + * map provided to the consumer is "live", so that the consumer can be used to + * {@linkplain MultiValueMap#set(Object, Object) overwrite} existing header values, + * {@linkplain MultiValueMap#remove(Object) remove} values, or use any of the other + * {@link MultiValueMap} methods. + * @param cookiesConsumer a function that consumes the cookies map + * @return this builder */ - EntityExchangeResult returnResult(Class elementClass); + Builder defaultCookies(Consumer> cookiesConsumer); /** - * Alternative to {@link #returnResult(Class)} that accepts information - * about a target type with generics. + * Apply the given {@code Consumer} to this builder instance. + *

This can be useful for applying pre-packaged customizations. + * @param builderConsumer the consumer to apply */ - EntityExchangeResult returnResult(ParameterizedTypeReference elementTypeRef); + Builder apply(Consumer> builderConsumer); /** - * {@link Consumer} of a {@link RestTestClient.ResponseSpec}. - * @see RestTestClient.ResponseSpec#expectAll(RestTestClient.ResponseSpec.ResponseSpecConsumer...) + * Build the {@link RestTestClient} instance. */ - @FunctionalInterface - interface ResponseSpecConsumer extends Consumer { - } + RestTestClient build(); } - /** - * Spec for expectations on the response body content. - */ - interface BodyContentSpec { - /** - * Assert the response body is empty and return the exchange result. - */ - EntityExchangeResult isEmpty(); - /** - * Parse the expected and actual response content as JSON and perform a - * comparison verifying that they contain the same attribute-value pairs - * regardless of formatting with lenient checking (extensible - * and non-strict array ordering). - *

Use of this method requires the - * JSONassert library - * to be on the classpath. - * @param expectedJson the expected JSON content - * @see #json(String, JsonCompareMode) - */ - default BodyContentSpec json(String expectedJson) { - return json(expectedJson, JsonCompareMode.LENIENT); - } - - /** - * Parse the expected and actual response content as JSON and perform a - * comparison using the given {@linkplain JsonCompareMode mode}. If the - * comparison failed, throws an {@link AssertionError} with the message - * of the {@link JsonComparison}. - *

Use of this method requires the - * JSONassert library - * to be on the classpath. - * @param expectedJson the expected JSON content - * @param compareMode the compare mode - * @see #json(String) - */ - BodyContentSpec json(String expectedJson, JsonCompareMode compareMode); - - /** - * Parse the expected and actual response content as JSON and perform a - * comparison using the given {@link JsonComparator}. If the comparison - * failed, throws an {@link AssertionError} with the message of the - * {@link JsonComparison}. - * @param expectedJson the expected JSON content - * @param comparator the comparator to use - */ - BodyContentSpec json(String expectedJson, JsonComparator comparator); - - /** - * Parse expected and actual response content as XML and assert that - * the two are "similar", i.e. they contain the same elements and - * attributes regardless of order. - *

Use of this method requires the - * XMLUnit library on - * the classpath. - * @param expectedXml the expected XML content. - * @see org.springframework.test.util.XmlExpectationsHelper#assertXmlEqual(String, String) - */ - BodyContentSpec xml(String expectedXml); - - /** - * Access to response body assertions using an XPath expression to - * inspect a specific subset of the body. - *

The XPath expression can be a parameterized string using - * formatting specifiers as defined in {@link String#format}. - * @param expression the XPath expression - * @param args arguments to parameterize the expression - * @see #xpath(String, Map, Object...) - */ - default XpathAssertions xpath(String expression, Object... args) { - return xpath(expression, null, args); - } - - /** - * Access to response body assertions with specific namespaces using an - * XPath expression to inspect a specific subset of the body. - *

The XPath expression can be a parameterized string using - * formatting specifiers as defined in {@link String#format}. - * @param expression the XPath expression - * @param namespaces the namespaces to use - * @param args arguments to parameterize the expression - */ - XpathAssertions xpath(String expression, @Nullable Map namespaces, Object... args); + interface MockServerBuilder extends Builder> { - /** - * Access to response body assertions using a - * JsonPath expression - * to inspect a specific subset of the body. - * @param expression the JsonPath expression - */ - JsonPathAssertions jsonPath(String expression); + MockServerBuilder configureServer(Consumer consumer); - /** - * Exit the chained API and return an {@code ExchangeResult} with the - * raw response content. - */ - EntityExchangeResult returnResult(); } - /** - * Spec for expectations on the response body decoded to a single Object. - * - * @param a self reference to the spec type - * @param the body type - */ - interface BodySpec> { - /** - * Transform the extracted the body with a function, for example, extracting a - * property, and assert the mapped value with a {@link Matcher}. - */ - T value(Function bodyMapper, Matcher matcher); - - /** - * Assert the extracted body with a {@link Consumer}. - */ - T value(Consumer consumer); - - /** - * Assert the exchange result with the given {@link Consumer}. - */ - T consumeWith(Consumer> consumer); - - /** - * Exit the chained API and return an {@code EntityExchangeResult} with the - * decoded response content. - */ - EntityExchangeResult returnResult(); - - /** - * Assert the extracted body is equal to the given value. - */ - T isEqualTo(B expected); - } /** * Specification for providing the URI of a request. @@ -424,6 +266,7 @@ public interface RestTestClient { * @param a self reference to the spec type */ interface UriSpec> { + /** * Specify the URI using an absolute, fully constructed {@link java.net.URI}. *

If a {@link UriBuilderFactory} was configured for the client with @@ -457,12 +300,9 @@ public interface RestTestClient { * @return spec to add headers or perform the exchange */ S uri(Function uriFunction); - } - - /** * Specification for adding request headers and performing an exchange. * @@ -564,6 +404,7 @@ public interface RestTestClient { ResponseSpec exchange(); } + /** * Specification for providing body of a request. */ @@ -587,70 +428,252 @@ public interface RestTestClient { RequestHeadersSpec body(Object body); } - interface Builder> { + + /** + * Specification for providing request headers and the URI of a request. + * + * @param a self reference to the spec type + */ + interface RequestHeadersUriSpec> extends UriSpec, RequestHeadersSpec { + } + + + /** + * Specification for providing the body and the URI of a request. + */ + interface RequestBodyUriSpec extends RequestBodySpec, RequestHeadersUriSpec { + } + + + /** + * Chained API for applying assertions to a response. + */ + interface ResponseSpec { + /** - * Apply the given {@code Consumer} to this builder instance. - *

This can be useful for applying pre-packaged customizations. - * @param builderConsumer the consumer to apply + * Apply multiple assertions to a response with the given + * {@linkplain RestTestClient.ResponseSpec.ResponseSpecConsumer consumers}, with the guarantee that + * all assertions will be applied even if one or more assertions fails + * with an exception. + *

If a single {@link Error} or {@link RuntimeException} is thrown, + * it will be rethrown. + *

If multiple exceptions are thrown, this method will throw an + * {@link AssertionError} whose error message is a summary of all the + * exceptions. In addition, each exception will be added as a + * {@linkplain Throwable#addSuppressed(Throwable) suppressed exception} to + * the {@code AssertionError}. + *

This feature is similar to the {@code SoftAssertions} support in + * AssertJ and the {@code assertAll()} support in JUnit Jupiter. + * + *

Example

+ *
+		 * restTestClient.get().uri("/hello").exchange()
+		 *     .expectAll(
+		 *         responseSpec -> responseSpec.expectStatus().isOk(),
+		 *         responseSpec -> responseSpec.expectBody(String.class).isEqualTo("Hello, World!")
+		 *     );
+		 * 
+ * @param consumers the list of {@code ResponseSpec} consumers */ - Builder apply(Consumer> builderConsumer); + ResponseSpec expectAll(ResponseSpecConsumer... consumers); /** - * Add the given cookie to all requests. - * @param cookieName the cookie name - * @param cookieValues the cookie values + * Assertions on the response status. */ - Builder defaultCookie(String cookieName, String... cookieValues); + StatusAssertions expectStatus(); /** - * Manipulate the default cookies with the given consumer. The - * map provided to the consumer is "live", so that the consumer can be used to - * {@linkplain MultiValueMap#set(Object, Object) overwrite} existing header values, - * {@linkplain MultiValueMap#remove(Object) remove} values, or use any of the other - * {@link MultiValueMap} methods. - * @param cookiesConsumer a function that consumes the cookies map - * @return this builder + * Assertions on the headers of the response. */ - Builder defaultCookies(Consumer> cookiesConsumer); + HeaderAssertions expectHeader(); /** - * Add the given header to all requests that haven't added it. - * @param headerName the header name - * @param headerValues the header values + * Assertions on the cookies of the response. */ - Builder defaultHeader(String headerName, String... headerValues); + CookieAssertions expectCookie(); /** - * Manipulate the default headers with the given consumer. The - * headers provided to the consumer are "live", so that the consumer can be used to - * {@linkplain HttpHeaders#set(String, String) overwrite} existing header values, - * {@linkplain HttpHeaders#remove(String) remove} values, or use any of the other - * {@link HttpHeaders} methods. - * @param headersConsumer a function that consumes the {@code HttpHeaders} - * @return this builder + * Consume and decode the response body to a single object of type + * {@code } and then apply assertions. + * @param bodyType the expected body type */ - Builder defaultHeaders(Consumer headersConsumer); + BodySpec expectBody(Class bodyType); /** - * Provide a pre-configured {@link UriBuilderFactory} instance as an - * alternative to and effectively overriding {@link #baseUrl(String)}. + * Alternative to {@link #expectBody(Class)} that accepts information + * about a target type with generics. */ - Builder uriBuilderFactory(UriBuilderFactory uriFactory); + BodySpec expectBody(ParameterizedTypeReference bodyType); /** - * Build the {@link RestTestClient} instance. + * Consume and decode the response body to {@code byte[]} and then apply + * assertions on the raw content (for example, isEmpty, JSONPath, etc.). */ - RestTestClient build(); + BodyContentSpec expectBody(); /** - * Configure a base URI as described in - * {@link RestClient#create(String) - * WebClient.create(String)}. + * Exit the chained flow in order to consume the response body + * externally. */ - Builder baseUrl(String baseUrl); + EntityExchangeResult returnResult(Class elementClass); + + /** + * Alternative to {@link #returnResult(Class)} that accepts information + * about a target type with generics. + */ + EntityExchangeResult returnResult(ParameterizedTypeReference elementTypeRef); + + /** + * {@link Consumer} of a {@link RestTestClient.ResponseSpec}. + * @see RestTestClient.ResponseSpec#expectAll(RestTestClient.ResponseSpec.ResponseSpecConsumer...) + */ + @FunctionalInterface + interface ResponseSpecConsumer extends Consumer { + } } - interface MockServerBuilder extends Builder> { - MockServerBuilder configureServer(Consumer consumer); + + /** + * Spec for expectations on the response body decoded to a single Object. + * + * @param a self reference to the spec type + * @param the body type + */ + interface BodySpec> { + + /** + * Assert the extracted body is equal to the given value. + */ + T isEqualTo(B expected); + + /** + * Transform the extracted the body with a function, for example, extracting a + * property, and assert the mapped value with a {@link Matcher}. + */ + T value(Function bodyMapper, Matcher matcher); + + /** + * Assert the extracted body with a {@link Consumer}. + */ + T value(Consumer consumer); + + /** + * Assert the exchange result with the given {@link Consumer}. + */ + T consumeWith(Consumer> consumer); + + /** + * Exit the chained API and return an {@code EntityExchangeResult} with the + * decoded response content. + */ + EntityExchangeResult returnResult(); + } + + + /** + * Spec for expectations on the response body content. + */ + interface BodyContentSpec { + /** + * Assert the response body is empty and return the exchange result. + */ + EntityExchangeResult isEmpty(); + + /** + * Parse the expected and actual response content as JSON and perform a + * comparison verifying that they contain the same attribute-value pairs + * regardless of formatting with lenient checking (extensible + * and non-strict array ordering). + *

Use of this method requires the + * JSONassert library + * to be on the classpath. + * @param expectedJson the expected JSON content + * @see #json(String, JsonCompareMode) + */ + default BodyContentSpec json(String expectedJson) { + return json(expectedJson, JsonCompareMode.LENIENT); + } + + /** + * Parse the expected and actual response content as JSON and perform a + * comparison using the given {@linkplain JsonCompareMode mode}. If the + * comparison failed, throws an {@link AssertionError} with the message + * of the {@link JsonComparison}. + *

Use of this method requires the + * JSONassert library + * to be on the classpath. + * @param expectedJson the expected JSON content + * @param compareMode the compare mode + * @see #json(String) + */ + BodyContentSpec json(String expectedJson, JsonCompareMode compareMode); + + /** + * Parse the expected and actual response content as JSON and perform a + * comparison using the given {@link JsonComparator}. If the comparison + * failed, throws an {@link AssertionError} with the message of the + * {@link JsonComparison}. + * @param expectedJson the expected JSON content + * @param comparator the comparator to use + */ + BodyContentSpec json(String expectedJson, JsonComparator comparator); + + /** + * Parse expected and actual response content as XML and assert that + * the two are "similar", i.e. they contain the same elements and + * attributes regardless of order. + *

Use of this method requires the + * XMLUnit library on + * the classpath. + * @param expectedXml the expected XML content. + * @see org.springframework.test.util.XmlExpectationsHelper#assertXmlEqual(String, String) + */ + BodyContentSpec xml(String expectedXml); + + /** + * Access to response body assertions using a + * JsonPath expression + * to inspect a specific subset of the body. + * @param expression the JsonPath expression + */ + JsonPathAssertions jsonPath(String expression); + + /** + * Access to response body assertions using an XPath expression to + * inspect a specific subset of the body. + *

The XPath expression can be a parameterized string using + * formatting specifiers as defined in {@link String#format}. + * @param expression the XPath expression + * @param args arguments to parameterize the expression + * @see #xpath(String, Map, Object...) + */ + default XpathAssertions xpath(String expression, Object... args) { + return xpath(expression, null, args); + } + + /** + * Access to response body assertions with specific namespaces using an + * XPath expression to inspect a specific subset of the body. + *

The XPath expression can be a parameterized string using + * formatting specifiers as defined in {@link String#format}. + * @param expression the XPath expression + * @param namespaces the namespaces to use + * @param args arguments to parameterize the expression + */ + XpathAssertions xpath(String expression, @Nullable Map namespaces, Object... args); + + /** + * Assert the response body content with the given {@link Consumer}. + * @param consumer the consumer for the response body; the input + * {@code byte[]} may be {@code null} if there was no response body. + */ + BodyContentSpec consumeWith(Consumer> consumer); + + /** + * Exit the chained API and return an {@code ExchangeResult} with the + * raw response content. + */ + EntityExchangeResult returnResult(); } + } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/client/StatusAssertions.java b/spring-test/src/main/java/org/springframework/test/web/servlet/client/StatusAssertions.java index debc3a4d3e6..c2f76ef22b0 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/client/StatusAssertions.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/client/StatusAssertions.java @@ -24,7 +24,7 @@ import org.springframework.test.web.support.AbstractStatusAssertions; * Assertions on the response status. * * @author Rob Worsnop - * + * @since 7.0 * @see ResponseSpec#expectStatus() */ public class StatusAssertions extends AbstractStatusAssertions { diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/client/XpathAssertions.java b/spring-test/src/main/java/org/springframework/test/web/servlet/client/XpathAssertions.java index d4bbaa27406..cfec64a17ee 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/client/XpathAssertions.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/client/XpathAssertions.java @@ -29,6 +29,7 @@ import org.springframework.util.Assert; * XPath assertions for the {@link RestTestClient}. * * @author Rob Worsnop + * @since 7.0 */ public class XpathAssertions extends AbstractXpathAssertions {