Skip to content

Commit 3f5eb77

Browse files
committed
Polish soft assertion support for WebTestClient
See spring-projectsgh-26969
1 parent d3c39c0 commit 3f5eb77

File tree

4 files changed

+106
-52
lines changed

4 files changed

+106
-52
lines changed

spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import java.nio.charset.StandardCharsets;
2222
import java.time.Duration;
2323
import java.time.ZonedDateTime;
24-
import java.util.ArrayList;
2524
import java.util.Arrays;
2625
import java.util.LinkedHashMap;
2726
import java.util.List;
@@ -45,6 +44,7 @@
4544
import org.springframework.http.client.reactive.ClientHttpRequest;
4645
import org.springframework.lang.Nullable;
4746
import org.springframework.test.util.AssertionErrors;
47+
import org.springframework.test.util.ExceptionCollector;
4848
import org.springframework.test.util.JsonExpectationsHelper;
4949
import org.springframework.test.util.XmlExpectationsHelper;
5050
import org.springframework.util.Assert;
@@ -64,6 +64,8 @@
6464
* Default implementation of {@link WebTestClient}.
6565
*
6666
* @author Rossen Stoyanchev
67+
* @author Sam Brannen
68+
* @author Michał Rowicki
6769
* @since 5.0
6870
*/
6971
class DefaultWebTestClient implements WebTestClient {
@@ -510,19 +512,25 @@ public <T> FluxExchangeResult<T> returnResult(ParameterizedTypeReference<T> elem
510512
}
511513

512514
@Override
513-
public ResponseSpec expectAllSoftly(ResponseSpecMatcher... asserts) {
514-
List<String> failedMessages = new ArrayList<>();
515-
for (int i = 0; i < asserts.length; i++) {
516-
ResponseSpecMatcher anAssert = asserts[i];
517-
try {
518-
anAssert.accept(this);
519-
}
520-
catch (AssertionError assertionException) {
521-
failedMessages.add("[" + i + "] " + assertionException.getMessage());
522-
}
515+
public ResponseSpec expectAll(ResponseSpecConsumer... consumers) {
516+
ExceptionCollector exceptionCollector = new ExceptionCollector();
517+
for (ResponseSpecConsumer consumer : consumers) {
518+
exceptionCollector.execute(() -> consumer.accept(this));
519+
}
520+
try {
521+
exceptionCollector.assertEmpty();
522+
}
523+
catch (RuntimeException ex) {
524+
throw ex;
523525
}
524-
if (!failedMessages.isEmpty()) {
525-
throw new AssertionError(String.join("\n", failedMessages));
526+
catch (Exception ex) {
527+
// In theory, a ResponseSpecConsumer should never throw an Exception
528+
// that is not a RuntimeException, but since ExceptionCollector may
529+
// throw a checked Exception, we handle this to appease the compiler
530+
// and in case someone uses a "sneaky throws" technique.
531+
AssertionError assertionError = new AssertionError(ex.getMessage());
532+
assertionError.initCause(ex);
533+
throw assertionError;
526534
}
527535
return this;
528536
}

spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@
8686
*
8787
* @author Rossen Stoyanchev
8888
* @author Brian Clozel
89+
* @author Sam Brannen
90+
* @author Michał Rowicki
8991
* @since 5.0
9092
* @see StatusAssertions
9193
* @see HeaderAssertions
@@ -781,6 +783,34 @@ interface RequestBodyUriSpec extends RequestBodySpec, RequestHeadersUriSpec<Requ
781783
*/
782784
interface ResponseSpec {
783785

786+
/**
787+
* Apply multiple assertions to a response with the given
788+
* {@linkplain ResponseSpecConsumer consumers}, with the guarantee that
789+
* all assertions will be applied even if one or more assertions fails
790+
* with an exception.
791+
* <p>If a single {@link Error} or {@link RuntimeException} is thrown,
792+
* it will be rethrown.
793+
* <p>If multiple exceptions are thrown, this method will throw an
794+
* {@link AssertionError} whose error message is a summary of all of the
795+
* exceptions. In addition, each exception will be added as a
796+
* {@linkplain Throwable#addSuppressed(Throwable) suppressed exception} to
797+
* the {@code AssertionError}.
798+
* <p>This feature is similar to the {@code SoftAssertions} support in
799+
* AssertJ and the {@code assertAll()} support in JUnit Jupiter.
800+
*
801+
* <h4>Example</h4>
802+
* <pre class="code">
803+
* webTestClient.get().uri("/hello").exchange()
804+
* .expectAll(
805+
* responseSpec -> responseSpec.expectStatus().isOk(),
806+
* responseSpec -> responseSpec.expectBody(String.class).isEqualTo("Hello, World!")
807+
* );
808+
* </pre>
809+
* @param consumers the list of {@code ResponseSpec} consumers
810+
* @since 5.3.10
811+
*/
812+
ResponseSpec expectAll(ResponseSpecConsumer... consumers);
813+
784814
/**
785815
* Assertions on the response status.
786816
*/
@@ -847,9 +877,14 @@ interface ResponseSpec {
847877
<T> FluxExchangeResult<T> returnResult(ParameterizedTypeReference<T> elementTypeRef);
848878

849879
/**
850-
* Array of assertions to test together a.k.a. soft assertions.
880+
* {@link Consumer} of a {@link ResponseSpec}.
881+
* @since 5.3.10
882+
* @see ResponseSpec#expectAll(ResponseSpecConsumer...)
851883
*/
852-
ResponseSpec expectAllSoftly(ResponseSpecMatcher... asserts);
884+
@FunctionalInterface
885+
interface ResponseSpecConsumer extends Consumer<ResponseSpec> {
886+
}
887+
853888
}
854889

855890

@@ -1011,5 +1046,4 @@ default XpathAssertions xpath(String expression, Object... args) {
10111046
EntityExchangeResult<byte[]> returnResult();
10121047
}
10131048

1014-
interface ResponseSpecMatcher extends Consumer<ResponseSpec> {}
10151049
}

spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/SoftAssertionTests.java

Lines changed: 23 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16+
1617
package org.springframework.test.web.reactive.server.samples;
1718

18-
import org.junit.jupiter.api.BeforeEach;
1919
import org.junit.jupiter.api.Test;
2020

2121
import org.springframework.test.web.reactive.server.WebTestClient;
@@ -25,51 +25,46 @@
2525
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
2626

2727
/**
28-
* Samples of tests using {@link WebTestClient} with soft assertions.
28+
* Integration tests for {@link WebTestClient} with soft assertions.
2929
*
3030
* @author Michał Rowicki
31-
* @since 5.3
31+
* @author Sam Brannen
32+
* @since 5.3.10
3233
*/
33-
public class SoftAssertionTests {
34-
35-
private WebTestClient client;
34+
class SoftAssertionTests {
3635

37-
38-
@BeforeEach
39-
public void setUp() throws Exception {
40-
this.client = WebTestClient.bindToController(new TestController()).build();
41-
}
36+
private final WebTestClient webTestClient = WebTestClient.bindToController(new TestController()).build();
4237

4338

4439
@Test
45-
public void test() throws Exception {
46-
this.client.get().uri("/test")
47-
.exchange()
48-
.expectAllSoftly(
49-
exchange -> exchange.expectStatus().isOk(),
50-
exchange -> exchange.expectBody(String.class).isEqualTo("It works!")
51-
);
40+
void expectAll() {
41+
this.webTestClient.get().uri("/test").exchange()
42+
.expectAll(
43+
responseSpec -> responseSpec.expectStatus().isOk(),
44+
responseSpec -> responseSpec.expectBody(String.class).isEqualTo("hello")
45+
);
5246
}
5347

5448
@Test
55-
public void testAllFails() throws Exception {
49+
void expectAllWithMultipleFailures() throws Exception {
5650
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
57-
this.client.get().uri("/test")
58-
.exchange()
59-
.expectAllSoftly(
60-
exchange -> exchange.expectStatus().isBadRequest(),
61-
exchange -> exchange.expectBody(String.class).isEqualTo("It won't work :(")
62-
)
63-
).withMessage("[0] Status expected:<400 BAD_REQUEST> but was:<200 OK>\n[1] Response body expected:<It won't work :(> but was:<It works!>");
51+
this.webTestClient.get().uri("/test").exchange()
52+
.expectAll(
53+
responseSpec -> responseSpec.expectStatus().isBadRequest(),
54+
responseSpec -> responseSpec.expectStatus().isOk(),
55+
responseSpec -> responseSpec.expectBody(String.class).isEqualTo("bogus")
56+
)
57+
).withMessage("Multiple Exceptions (2):\nStatus expected:<400 BAD_REQUEST> but was:<200 OK>\nResponse body expected:<bogus> but was:<hello>");
6458
}
6559

6660

6761
@RestController
6862
static class TestController {
6963

7064
@GetMapping("/test")
71-
public String handle() {
72-
return "It works!";
65+
String handle() {
66+
return "hello";
7367
}
7468
}
69+
7570
}

src/docs/asciidoc/testing/testing-webtestclient.adoc

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -262,19 +262,36 @@ To assert the response status and headers, use the following:
262262
.Java
263263
----
264264
client.get().uri("/persons/1")
265-
.accept(MediaType.APPLICATION_JSON)
266-
.exchange()
267-
.expectStatus().isOk()
268-
.expectHeader().contentType(MediaType.APPLICATION_JSON)
265+
.accept(MediaType.APPLICATION_JSON)
266+
.exchange()
267+
.expectStatus().isOk()
268+
.expectHeader().contentType(MediaType.APPLICATION_JSON);
269269
----
270270
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
271271
.Kotlin
272272
----
273273
client.get().uri("/persons/1")
274-
.accept(MediaType.APPLICATION_JSON)
275-
.exchange()
276-
.expectStatus().isOk()
277-
.expectHeader().contentType(MediaType.APPLICATION_JSON)
274+
.accept(MediaType.APPLICATION_JSON)
275+
.exchange()
276+
.expectStatus().isOk()
277+
.expectHeader().contentType(MediaType.APPLICATION_JSON)
278+
----
279+
280+
If you would like for all expectations to be asserted even if one of them fails, you can
281+
use `expectAll(..)` instead of multiple chained `expect*(..)` calls. This feature is
282+
similar to the _soft assertions_ support in AssertJ and the `assertAll()` support in
283+
JUnit Jupiter.
284+
285+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
286+
.Java
287+
----
288+
client.get().uri("/persons/1")
289+
.accept(MediaType.APPLICATION_JSON)
290+
.exchange()
291+
.expectAll(
292+
spec -> spec.expectStatus().isOk(),
293+
spec -> spec.expectHeader().contentType(MediaType.APPLICATION_JSON)
294+
);
278295
----
279296

280297
You can then choose to decode the response body through one of the following:

0 commit comments

Comments
 (0)