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.nio.charset.StandardCharsets;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
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.http.client.reactive.ClientHttpRequest;
|
||||||
import org.springframework.lang.Nullable;
|
import org.springframework.lang.Nullable;
|
||||||
import org.springframework.test.util.AssertionErrors;
|
import org.springframework.test.util.AssertionErrors;
|
||||||
|
import org.springframework.test.util.ExceptionCollector;
|
||||||
import org.springframework.test.util.JsonExpectationsHelper;
|
import org.springframework.test.util.JsonExpectationsHelper;
|
||||||
import org.springframework.test.util.XmlExpectationsHelper;
|
import org.springframework.test.util.XmlExpectationsHelper;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
|
@ -64,6 +64,8 @@ import org.springframework.web.util.UriBuilderFactory;
|
||||||
* Default implementation of {@link WebTestClient}.
|
* Default implementation of {@link WebTestClient}.
|
||||||
*
|
*
|
||||||
* @author Rossen Stoyanchev
|
* @author Rossen Stoyanchev
|
||||||
|
* @author Sam Brannen
|
||||||
|
* @author Michał Rowicki
|
||||||
* @since 5.0
|
* @since 5.0
|
||||||
*/
|
*/
|
||||||
class DefaultWebTestClient implements WebTestClient {
|
class DefaultWebTestClient implements WebTestClient {
|
||||||
|
|
@ -510,19 +512,25 @@ class DefaultWebTestClient implements WebTestClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ResponseSpec expectAllSoftly(ResponseSpecMatcher... asserts) {
|
public ResponseSpec expectAll(ResponseSpecConsumer... consumers) {
|
||||||
List<String> failedMessages = new ArrayList<>();
|
ExceptionCollector exceptionCollector = new ExceptionCollector();
|
||||||
for (int i = 0; i < asserts.length; i++) {
|
for (ResponseSpecConsumer consumer : consumers) {
|
||||||
ResponseSpecMatcher anAssert = asserts[i];
|
exceptionCollector.execute(() -> consumer.accept(this));
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
anAssert.accept(this);
|
exceptionCollector.assertEmpty();
|
||||||
}
|
}
|
||||||
catch (AssertionError assertionException) {
|
catch (RuntimeException ex) {
|
||||||
failedMessages.add("[" + i + "] " + assertionException.getMessage());
|
throw ex;
|
||||||
}
|
}
|
||||||
}
|
catch (Exception ex) {
|
||||||
if (!failedMessages.isEmpty()) {
|
// In theory, a ResponseSpecConsumer should never throw an Exception
|
||||||
throw new AssertionError(String.join("\n", failedMessages));
|
// 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;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,8 @@ import org.springframework.web.util.UriBuilderFactory;
|
||||||
*
|
*
|
||||||
* @author Rossen Stoyanchev
|
* @author Rossen Stoyanchev
|
||||||
* @author Brian Clozel
|
* @author Brian Clozel
|
||||||
|
* @author Sam Brannen
|
||||||
|
* @author Michał Rowicki
|
||||||
* @since 5.0
|
* @since 5.0
|
||||||
* @see StatusAssertions
|
* @see StatusAssertions
|
||||||
* @see HeaderAssertions
|
* @see HeaderAssertions
|
||||||
|
|
@ -781,6 +783,34 @@ public interface WebTestClient {
|
||||||
*/
|
*/
|
||||||
interface ResponseSpec {
|
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.
|
* Assertions on the response status.
|
||||||
*/
|
*/
|
||||||
|
|
@ -847,9 +877,14 @@ public interface WebTestClient {
|
||||||
<T> FluxExchangeResult<T> returnResult(ParameterizedTypeReference<T> elementTypeRef);
|
<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();
|
EntityExchangeResult<byte[]> returnResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ResponseSpecMatcher extends Consumer<ResponseSpec> {}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,9 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.springframework.test.web.reactive.server.samples;
|
package org.springframework.test.web.reactive.server.samples;
|
||||||
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
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;
|
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
|
* @author Michał Rowicki
|
||||||
* @since 5.3
|
* @author Sam Brannen
|
||||||
|
* @since 5.3.10
|
||||||
*/
|
*/
|
||||||
public class SoftAssertionTests {
|
class SoftAssertionTests {
|
||||||
|
|
||||||
private WebTestClient client;
|
private final WebTestClient webTestClient = WebTestClient.bindToController(new TestController()).build();
|
||||||
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
public void setUp() throws Exception {
|
|
||||||
this.client = WebTestClient.bindToController(new TestController()).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void test() throws Exception {
|
void expectAll() {
|
||||||
this.client.get().uri("/test")
|
this.webTestClient.get().uri("/test").exchange()
|
||||||
.exchange()
|
.expectAll(
|
||||||
.expectAllSoftly(
|
responseSpec -> responseSpec.expectStatus().isOk(),
|
||||||
exchange -> exchange.expectStatus().isOk(),
|
responseSpec -> responseSpec.expectBody(String.class).isEqualTo("hello")
|
||||||
exchange -> exchange.expectBody(String.class).isEqualTo("It works!")
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testAllFails() throws Exception {
|
void expectAllWithMultipleFailures() throws Exception {
|
||||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
|
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
|
||||||
this.client.get().uri("/test")
|
this.webTestClient.get().uri("/test").exchange()
|
||||||
.exchange()
|
.expectAll(
|
||||||
.expectAllSoftly(
|
responseSpec -> responseSpec.expectStatus().isBadRequest(),
|
||||||
exchange -> exchange.expectStatus().isBadRequest(),
|
responseSpec -> responseSpec.expectStatus().isOk(),
|
||||||
exchange -> exchange.expectBody(String.class).isEqualTo("It won't work :(")
|
responseSpec -> responseSpec.expectBody(String.class).isEqualTo("bogus")
|
||||||
)
|
)
|
||||||
).withMessage("[0] Status expected:<400 BAD_REQUEST> but was:<200 OK>\n[1] Response body expected:<It won't work :(> but was:<It works!>");
|
).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 {
|
static class TestController {
|
||||||
|
|
||||||
@GetMapping("/test")
|
@GetMapping("/test")
|
||||||
public String handle() {
|
String handle() {
|
||||||
return "It works!";
|
return "hello";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -265,7 +265,7 @@ To assert the response status and headers, use the following:
|
||||||
.accept(MediaType.APPLICATION_JSON)
|
.accept(MediaType.APPLICATION_JSON)
|
||||||
.exchange()
|
.exchange()
|
||||||
.expectStatus().isOk()
|
.expectStatus().isOk()
|
||||||
.expectHeader().contentType(MediaType.APPLICATION_JSON)
|
.expectHeader().contentType(MediaType.APPLICATION_JSON);
|
||||||
----
|
----
|
||||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||||
.Kotlin
|
.Kotlin
|
||||||
|
|
@ -277,6 +277,23 @@ To assert the response status and headers, use the following:
|
||||||
.expectHeader().contentType(MediaType.APPLICATION_JSON)
|
.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:
|
You can then choose to decode the response body through one of the following:
|
||||||
|
|
||||||
* `expectBody(Class<T>)`: Decode to single object.
|
* `expectBody(Class<T>)`: Decode to single object.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue