From 3c2dfebf4ec5e9d791c1f3c9fa0ad35a5a9fcd6b Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Mon, 23 Aug 2021 18:59:41 +0200 Subject: [PATCH] Polish soft assertion support for WebTestClient See gh-26969 --- .../reactive/server/DefaultWebTestClient.java | 34 ++++++++----- .../web/reactive/server/WebTestClient.java | 40 +++++++++++++-- .../server/samples/SoftAssertionTests.java | 51 +++++++++---------- .../testing/testing-webtestclient.adoc | 33 +++++++++--- 4 files changed, 106 insertions(+), 52 deletions(-) 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 06dab2ea38a..8628ef26226 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 @@ -21,7 +21,6 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.ZonedDateTime; -import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; @@ -45,6 +44,7 @@ import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.http.client.reactive.ClientHttpRequest; import org.springframework.lang.Nullable; import org.springframework.test.util.AssertionErrors; +import org.springframework.test.util.ExceptionCollector; import org.springframework.test.util.JsonExpectationsHelper; import org.springframework.test.util.XmlExpectationsHelper; import org.springframework.util.Assert; @@ -64,6 +64,8 @@ import org.springframework.web.util.UriBuilderFactory; * Default implementation of {@link WebTestClient}. * * @author Rossen Stoyanchev + * @author Sam Brannen + * @author Michał Rowicki * @since 5.0 */ class DefaultWebTestClient implements WebTestClient { @@ -510,19 +512,25 @@ class DefaultWebTestClient implements WebTestClient { } @Override - public ResponseSpec expectAllSoftly(ResponseSpecMatcher... asserts) { - List failedMessages = new ArrayList<>(); - for (int i = 0; i < asserts.length; i++) { - ResponseSpecMatcher anAssert = asserts[i]; - try { - anAssert.accept(this); - } - catch (AssertionError assertionException) { - failedMessages.add("[" + i + "] " + assertionException.getMessage()); - } + public ResponseSpec expectAll(ResponseSpecConsumer... consumers) { + ExceptionCollector exceptionCollector = new ExceptionCollector(); + for (ResponseSpecConsumer consumer : consumers) { + exceptionCollector.execute(() -> consumer.accept(this)); } - if (!failedMessages.isEmpty()) { - throw new AssertionError(String.join("\n", failedMessages)); + try { + exceptionCollector.assertEmpty(); + } + catch (RuntimeException ex) { + throw ex; + } + catch (Exception ex) { + // In theory, a ResponseSpecConsumer should never throw an Exception + // that is not a RuntimeException, but since ExceptionCollector may + // throw a checked Exception, we handle this to appease the compiler + // and in case someone uses a "sneaky throws" technique. + AssertionError assertionError = new AssertionError(ex.getMessage()); + assertionError.initCause(ex); + throw assertionError; } return this; } 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 8f0159aa70a..84adf85013c 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 @@ -86,6 +86,8 @@ import org.springframework.web.util.UriBuilderFactory; * * @author Rossen Stoyanchev * @author Brian Clozel + * @author Sam Brannen + * @author Michał Rowicki * @since 5.0 * @see StatusAssertions * @see HeaderAssertions @@ -781,6 +783,34 @@ public interface WebTestClient { */ interface ResponseSpec { + /** + * Apply multiple assertions to a response with the given + * {@linkplain 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 of 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

+ *
+		 * webTestClient.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 + * @since 5.3.10 + */ + ResponseSpec expectAll(ResponseSpecConsumer... consumers); + /** * Assertions on the response status. */ @@ -847,9 +877,14 @@ public interface WebTestClient { FluxExchangeResult returnResult(ParameterizedTypeReference elementTypeRef); /** - * Array of assertions to test together a.k.a. soft assertions. + * {@link Consumer} of a {@link ResponseSpec}. + * @since 5.3.10 + * @see ResponseSpec#expectAll(ResponseSpecConsumer...) */ - ResponseSpec expectAllSoftly(ResponseSpecMatcher... asserts); + @FunctionalInterface + interface ResponseSpecConsumer extends Consumer { + } + } @@ -1011,5 +1046,4 @@ public interface WebTestClient { EntityExchangeResult returnResult(); } - interface ResponseSpecMatcher extends Consumer {} } diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/SoftAssertionTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/SoftAssertionTests.java index 17d5cc1a3d3..a4d4bb8c72f 100644 --- a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/SoftAssertionTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/SoftAssertionTests.java @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.test.web.reactive.server.samples; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.test.web.reactive.server.WebTestClient; @@ -25,42 +25,36 @@ import org.springframework.web.bind.annotation.RestController; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** - * Samples of tests using {@link WebTestClient} with soft assertions. + * Integration tests for {@link WebTestClient} with soft assertions. * * @author Michał Rowicki - * @since 5.3 + * @author Sam Brannen + * @since 5.3.10 */ -public class SoftAssertionTests { +class SoftAssertionTests { - private WebTestClient client; - - - @BeforeEach - public void setUp() throws Exception { - this.client = WebTestClient.bindToController(new TestController()).build(); - } + private final WebTestClient webTestClient = WebTestClient.bindToController(new TestController()).build(); @Test - public void test() throws Exception { - this.client.get().uri("/test") - .exchange() - .expectAllSoftly( - exchange -> exchange.expectStatus().isOk(), - exchange -> exchange.expectBody(String.class).isEqualTo("It works!") - ); + void expectAll() { + this.webTestClient.get().uri("/test").exchange() + .expectAll( + responseSpec -> responseSpec.expectStatus().isOk(), + responseSpec -> responseSpec.expectBody(String.class).isEqualTo("hello") + ); } @Test - public void testAllFails() throws Exception { + void expectAllWithMultipleFailures() throws Exception { assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> - this.client.get().uri("/test") - .exchange() - .expectAllSoftly( - exchange -> exchange.expectStatus().isBadRequest(), - exchange -> exchange.expectBody(String.class).isEqualTo("It won't work :(") - ) - ).withMessage("[0] Status expected:<400 BAD_REQUEST> but was:<200 OK>\n[1] Response body expected: but was:"); + this.webTestClient.get().uri("/test").exchange() + .expectAll( + responseSpec -> responseSpec.expectStatus().isBadRequest(), + responseSpec -> responseSpec.expectStatus().isOk(), + responseSpec -> responseSpec.expectBody(String.class).isEqualTo("bogus") + ) + ).withMessage("Multiple Exceptions (2):\nStatus expected:<400 BAD_REQUEST> but was:<200 OK>\nResponse body expected: but was:"); } @@ -68,8 +62,9 @@ public class SoftAssertionTests { static class TestController { @GetMapping("/test") - public String handle() { - return "It works!"; + String handle() { + return "hello"; } } + } diff --git a/src/docs/asciidoc/testing/testing-webtestclient.adoc b/src/docs/asciidoc/testing/testing-webtestclient.adoc index 6c5dfb80da5..4335760e104 100644 --- a/src/docs/asciidoc/testing/testing-webtestclient.adoc +++ b/src/docs/asciidoc/testing/testing-webtestclient.adoc @@ -262,19 +262,36 @@ To assert the response status and headers, use the following: .Java ---- client.get().uri("/persons/1") - .accept(MediaType.APPLICATION_JSON) - .exchange() - .expectStatus().isOk() - .expectHeader().contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isOk() + .expectHeader().contentType(MediaType.APPLICATION_JSON); ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] .Kotlin ---- client.get().uri("/persons/1") - .accept(MediaType.APPLICATION_JSON) - .exchange() - .expectStatus().isOk() - .expectHeader().contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isOk() + .expectHeader().contentType(MediaType.APPLICATION_JSON) +---- + +If you would like for all expectations to be asserted even if one of them fails, you can +use `expectAll(..)` instead of multiple chained `expect*(..)` calls. This feature is +similar to the _soft assertions_ support in AssertJ and the `assertAll()` support in +JUnit Jupiter. + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + client.get().uri("/persons/1") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectAll( + spec -> spec.expectStatus().isOk(), + spec -> spec.expectHeader().contentType(MediaType.APPLICATION_JSON) + ); ---- You can then choose to decode the response body through one of the following: