Polish soft assertion support for WebTestClient

See gh-26969
This commit is contained in:
Sam Brannen 2021-08-23 18:59:41 +02:00
parent 25dca40413
commit 3c2dfebf4e
4 changed files with 106 additions and 52 deletions

View File

@ -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<String> 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;
}

View File

@ -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.
* <p>If a single {@link Error} or {@link RuntimeException} is thrown,
* it will be rethrown.
* <p>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}.
* <p>This feature is similar to the {@code SoftAssertions} support in
* AssertJ and the {@code assertAll()} support in JUnit Jupiter.
*
* <h4>Example</h4>
* <pre class="code">
* webTestClient.get().uri("/hello").exchange()
* .expectAll(
* responseSpec -> responseSpec.expectStatus().isOk(),
* responseSpec -> responseSpec.expectBody(String.class).isEqualTo("Hello, World!")
* );
* </pre>
* @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 {
<T> FluxExchangeResult<T> returnResult(ParameterizedTypeReference<T> 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<ResponseSpec> {
}
}
@ -1011,5 +1046,4 @@ public interface WebTestClient {
EntityExchangeResult<byte[]> returnResult();
}
interface ResponseSpecMatcher extends Consumer<ResponseSpec> {}
}

View File

@ -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:<It won't work :(> but was:<It works!>");
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:<bogus> but was:<hello>");
}
@ -68,8 +62,9 @@ public class SoftAssertionTests {
static class TestController {
@GetMapping("/test")
public String handle() {
return "It works!";
String handle() {
return "hello";
}
}
}

View File

@ -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: