Polish soft assertion support for WebTestClient
See gh-26969
This commit is contained in:
parent
25dca40413
commit
3c2dfebf4e
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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> {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in New Issue