From 91122ec0f507bf8b60bd4d6fa3e4aadf584b115d Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 20 Jul 2018 22:28:40 -0400 Subject: [PATCH] Add Consumer methods to WebTestClient assertion classes Issue: SPR-16574 --- .../test/util/JsonPathExpectationsHelper.java | 69 +++++--- .../test/util/XpathExpectationsHelper.java | 166 +++++++++++------- .../reactive/server/DefaultWebTestClient.java | 6 + .../web/reactive/server/HeaderAssertions.java | 13 ++ .../reactive/server/JsonPathAssertions.java | 24 +++ .../web/reactive/server/StatusAssertions.java | 13 ++ .../web/reactive/server/WebTestClient.java | 6 + .../web/reactive/server/XpathAssertions.java | 37 ++++ .../server/samples/ResponseEntityTests.java | 13 ++ 9 files changed, 251 insertions(+), 96 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/util/JsonPathExpectationsHelper.java b/spring-test/src/main/java/org/springframework/test/util/JsonPathExpectationsHelper.java index 7779e38e89d..bdb978aabbd 100644 --- a/spring-test/src/main/java/org/springframework/test/util/JsonPathExpectationsHelper.java +++ b/spring-test/src/main/java/org/springframework/test/util/JsonPathExpectationsHelper.java @@ -20,19 +20,15 @@ import java.util.List; import java.util.Map; import com.jayway.jsonpath.JsonPath; +import org.hamcrest.CoreMatchers; import org.hamcrest.Matcher; +import org.hamcrest.MatcherAssert; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.core.IsInstanceOf.instanceOf; -import static org.springframework.test.util.AssertionErrors.assertEquals; -import static org.springframework.test.util.AssertionErrors.assertTrue; -import static org.springframework.test.util.AssertionErrors.fail; - /** * A helper class for applying assertions via JSON path expressions. * @@ -74,7 +70,7 @@ public class JsonPathExpectationsHelper { @SuppressWarnings("unchecked") public void assertValue(String content, Matcher matcher) { T value = (T) evaluateJsonPath(content); - assertThat("JSON path \"" + this.expression + "\"", value, matcher); + MatcherAssert.assertThat("JSON path \"" + this.expression + "\"", value, matcher); } /** @@ -89,7 +85,7 @@ public class JsonPathExpectationsHelper { @SuppressWarnings("unchecked") public void assertValue(String content, Matcher matcher, Class targetType) { T value = (T) evaluateJsonPath(content, targetType); - assertThat("JSON path \"" + this.expression + "\"", value, matcher); + MatcherAssert.assertThat("JSON path \"" + this.expression + "\"", value, matcher); } /** @@ -104,10 +100,11 @@ public class JsonPathExpectationsHelper { @SuppressWarnings("rawtypes") List actualValueList = (List) actualValue; if (actualValueList.isEmpty()) { - fail("No matching value at JSON path \"" + this.expression + "\""); + AssertionErrors.fail("No matching value at JSON path \"" + this.expression + "\""); } if (actualValueList.size() != 1) { - fail("Got a list of values " + actualValue + " instead of the expected single value " + expectedValue); + AssertionErrors.fail("Got a list of values " + actualValue + + " instead of the expected single value " + expectedValue); } actualValue = actualValueList.get(0); } @@ -116,7 +113,7 @@ public class JsonPathExpectationsHelper { actualValue = evaluateJsonPath(content, expectedValue.getClass()); } } - assertEquals("JSON path \"" + this.expression + "\"", expectedValue, actualValue); + AssertionErrors.assertEquals("JSON path \"" + this.expression + "\"", expectedValue, actualValue); } /** @@ -127,7 +124,7 @@ public class JsonPathExpectationsHelper { */ public void assertValueIsString(String content) { Object value = assertExistsAndReturn(content); - assertThat(failureReason("a string", value), value, instanceOf(String.class)); + MatcherAssert.assertThat(failureReason("a string", value), value, CoreMatchers.instanceOf(String.class)); } /** @@ -138,7 +135,7 @@ public class JsonPathExpectationsHelper { */ public void assertValueIsBoolean(String content) { Object value = assertExistsAndReturn(content); - assertThat(failureReason("a boolean", value), value, instanceOf(Boolean.class)); + MatcherAssert.assertThat(failureReason("a boolean", value), value, CoreMatchers.instanceOf(Boolean.class)); } /** @@ -149,7 +146,7 @@ public class JsonPathExpectationsHelper { */ public void assertValueIsNumber(String content) { Object value = assertExistsAndReturn(content); - assertThat(failureReason("a number", value), value, instanceOf(Number.class)); + MatcherAssert.assertThat(failureReason("a number", value), value, CoreMatchers.instanceOf(Number.class)); } /** @@ -159,7 +156,7 @@ public class JsonPathExpectationsHelper { */ public void assertValueIsArray(String content) { Object value = assertExistsAndReturn(content); - assertThat(failureReason("an array", value), value, instanceOf(List.class)); + MatcherAssert.assertThat(failureReason("an array", value), value, CoreMatchers.instanceOf(List.class)); } /** @@ -170,7 +167,7 @@ public class JsonPathExpectationsHelper { */ public void assertValueIsMap(String content) { Object value = assertExistsAndReturn(content); - assertThat(failureReason("a map", value), value, instanceOf(Map.class)); + MatcherAssert.assertThat(failureReason("a map", value), value, CoreMatchers.instanceOf(Map.class)); } /** @@ -204,10 +201,10 @@ public class JsonPathExpectationsHelper { } String reason = failureReason("no value", value); if (pathIsIndefinite() && value instanceof List) { - assertTrue(reason, ((List) value).isEmpty()); + AssertionErrors.assertTrue(reason, ((List) value).isEmpty()); } else { - assertTrue(reason, (value == null)); + AssertionErrors.assertTrue(reason, (value == null)); } } @@ -220,7 +217,7 @@ public class JsonPathExpectationsHelper { */ public void assertValueIsEmpty(String content) { Object value = evaluateJsonPath(content); - assertTrue(failureReason("an empty value", value), ObjectUtils.isEmpty(value)); + AssertionErrors.assertTrue(failureReason("an empty value", value), ObjectUtils.isEmpty(value)); } /** @@ -232,7 +229,7 @@ public class JsonPathExpectationsHelper { */ public void assertValueIsNotEmpty(String content) { Object value = evaluateJsonPath(content); - assertTrue(failureReason("a non-empty value", value), !ObjectUtils.isEmpty(value)); + AssertionErrors.assertTrue(failureReason("a non-empty value", value), !ObjectUtils.isEmpty(value)); } /** @@ -247,7 +244,8 @@ public class JsonPathExpectationsHelper { public void hasJsonPath(String content) { Object value = evaluateJsonPath(content); if (pathIsIndefinite() && value instanceof List) { - assertTrue("No values for JSON path \"" + this.expression + "\"", !((List) value).isEmpty()); + String message = "No values for JSON path \"" + this.expression + "\""; + AssertionErrors.assertTrue(message, !((List) value).isEmpty()); } } @@ -270,10 +268,10 @@ public class JsonPathExpectationsHelper { return; } if (pathIsIndefinite() && value instanceof List) { - assertTrue(failureReason("no values", value), ((List) value).isEmpty()); + AssertionErrors.assertTrue(failureReason("no values", value), ((List) value).isEmpty()); } else { - fail(failureReason("no value", value)); + AssertionErrors.fail(failureReason("no value", value)); } } @@ -282,18 +280,31 @@ public class JsonPathExpectationsHelper { ObjectUtils.nullSafeToString(StringUtils.quoteIfString(value))); } + /** + * Evaluate the JSON path and return the resulting value. + * @param content the content to evaluate against + * @return the result of the evaluation + * @throws AssertionError if the evaluation fails + */ @Nullable - private Object evaluateJsonPath(String content) { + public Object evaluateJsonPath(String content) { try { return this.jsonPath.read(content); } catch (Throwable ex) { - String message = "No value at JSON path \"" + this.expression + "\""; - throw new AssertionError(message, ex); + throw new AssertionError("No value at JSON path \"" + this.expression + "\"", ex); } } - private Object evaluateJsonPath(String content, Class targetType) { + /** + * Variant of {@link #evaluateJsonPath(String)} with a target type. + * This can be useful for matching numbers reliably for example coercing an + * integer into a double. + * @param content the content to evaluate against + * @return the result of the evaluation + * @throws AssertionError if the evaluation fails + */ + public Object evaluateJsonPath(String content, Class targetType) { try { return JsonPath.parse(content).read(this.expression, targetType); } @@ -307,9 +318,9 @@ public class JsonPathExpectationsHelper { private Object assertExistsAndReturn(String content) { Object value = evaluateJsonPath(content); String reason = "No value at JSON path \"" + this.expression + "\""; - assertTrue(reason, value != null); + AssertionErrors.assertTrue(reason, value != null); if (pathIsIndefinite() && value instanceof List) { - assertTrue(reason, !((List) value).isEmpty()); + AssertionErrors.assertTrue(reason, !((List) value).isEmpty()); } return value; } diff --git a/spring-test/src/main/java/org/springframework/test/util/XpathExpectationsHelper.java b/spring-test/src/main/java/org/springframework/test/util/XpathExpectationsHelper.java index b8edd30096c..881e8d4853b 100644 --- a/spring-test/src/main/java/org/springframework/test/util/XpathExpectationsHelper.java +++ b/spring-test/src/main/java/org/springframework/test/util/XpathExpectationsHelper.java @@ -29,6 +29,7 @@ import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import org.hamcrest.Matcher; +import org.hamcrest.MatcherAssert; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; @@ -39,9 +40,6 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.util.xml.SimpleNamespaceContext; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.springframework.test.util.AssertionErrors.assertEquals; -import static org.springframework.test.util.AssertionErrors.assertTrue; /** * A helper class for applying assertions via XPath expressions. @@ -74,9 +72,8 @@ public class XpathExpectationsHelper { this.hasNamespaces = !CollectionUtils.isEmpty(namespaces); } - - private XPathExpression compileXpathExpression(String expression, @Nullable Map namespaces) - throws XPathExpressionException { + private static XPathExpression compileXpathExpression(String expression, + @Nullable Map namespaces) throws XPathExpressionException { SimpleNamespaceContext namespaceContext = new SimpleNamespaceContext(); namespaceContext.setBindings(namespaces != null ? namespaces : Collections.emptyMap()); @@ -85,6 +82,7 @@ public class XpathExpectationsHelper { return xpath.compile(expression); } + /** * Return the compiled XPath expression. */ @@ -92,6 +90,7 @@ public class XpathExpectationsHelper { return this.xpathExpression; } + /** * Parse the content, evaluate the XPath expression as a {@link Node}, * and assert it with the given {@code Matcher}. @@ -99,38 +98,8 @@ public class XpathExpectationsHelper { public void assertNode(byte[] content, @Nullable String encoding, final Matcher matcher) throws Exception { - Document document = parseXmlByteArray(content, encoding); - Node node = evaluateXpath(document, XPathConstants.NODE, Node.class); - assertThat("XPath " + this.expression, node, matcher); - } - - /** - * Parse the given XML content to a {@link Document}. - * @param xml the content to parse - * @param encoding optional content encoding, if provided as metadata (e.g. in HTTP headers) - * @return the parsed document - */ - protected Document parseXmlByteArray(byte[] xml, @Nullable String encoding) throws Exception { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setNamespaceAware(this.hasNamespaces); - DocumentBuilder documentBuilder = factory.newDocumentBuilder(); - InputSource inputSource = new InputSource(new ByteArrayInputStream(xml)); - if (StringUtils.hasText(encoding)) { - inputSource.setEncoding(encoding); - } - return documentBuilder.parse(inputSource); - } - - /** - * Apply the XPath expression to given document. - * @throws XPathExpressionException if expression evaluation failed - */ - @SuppressWarnings("unchecked") - @Nullable - protected T evaluateXpath(Document document, QName evaluationType, Class expectedClass) - throws XPathExpressionException { - - return (T) getXpathExpression().evaluate(document, evaluationType); + Node node = evaluateXpath(content, encoding, Node.class); + MatcherAssert.assertThat("XPath " + this.expression, node, matcher); } /** @@ -138,9 +107,8 @@ public class XpathExpectationsHelper { * @throws Exception if content parsing or expression evaluation fails */ public void exists(byte[] content, @Nullable String encoding) throws Exception { - Document document = parseXmlByteArray(content, encoding); - Node node = evaluateXpath(document, XPathConstants.NODE, Node.class); - assertTrue("XPath " + this.expression + " does not exist", node != null); + Node node = evaluateXpath(content, encoding, Node.class); + AssertionErrors.assertTrue("XPath " + this.expression + " does not exist", node != null); } /** @@ -148,9 +116,8 @@ public class XpathExpectationsHelper { * @throws Exception if content parsing or expression evaluation fails */ public void doesNotExist(byte[] content, @Nullable String encoding) throws Exception { - Document document = parseXmlByteArray(content, encoding); - Node node = evaluateXpath(document, XPathConstants.NODE, Node.class); - assertTrue("XPath " + this.expression + " exists", node == null); + Node node = evaluateXpath(content, encoding, Node.class); + AssertionErrors.assertTrue("XPath " + this.expression + " exists", node == null); } /** @@ -158,11 +125,12 @@ public class XpathExpectationsHelper { * given Hamcrest matcher. * @throws Exception if content parsing or expression evaluation fails */ - public void assertNodeCount(byte[] content, @Nullable String encoding, Matcher matcher) throws Exception { - Document document = parseXmlByteArray(content, encoding); - NodeList nodeList = evaluateXpath(document, XPathConstants.NODESET, NodeList.class); - assertThat("nodeCount for XPath " + this.expression, - (nodeList != null ? nodeList.getLength() : 0), matcher); + public void assertNodeCount(byte[] content, @Nullable String encoding, Matcher matcher) + throws Exception { + + NodeList nodeList = evaluateXpath(content, encoding, NodeList.class); + String reason = "nodeCount for XPath " + this.expression; + MatcherAssert.assertThat(reason, nodeList != null ? nodeList.getLength() : 0, matcher); } /** @@ -170,9 +138,8 @@ public class XpathExpectationsHelper { * @throws Exception if content parsing or expression evaluation fails */ public void assertNodeCount(byte[] content, @Nullable String encoding, int expectedCount) throws Exception { - Document document = parseXmlByteArray(content, encoding); - NodeList nodeList = evaluateXpath(document, XPathConstants.NODESET, NodeList.class); - assertEquals("nodeCount for XPath " + this.expression, expectedCount, + NodeList nodeList = evaluateXpath(content, encoding, NodeList.class); + AssertionErrors.assertEquals("nodeCount for XPath " + this.expression, expectedCount, (nodeList != null ? nodeList.getLength() : 0)); } @@ -181,10 +148,11 @@ public class XpathExpectationsHelper { * given Hamcrest matcher. * @throws Exception if content parsing or expression evaluation fails */ - public void assertString(byte[] content, @Nullable String encoding, Matcher matcher) throws Exception { - Document document = parseXmlByteArray(content, encoding); - String result = evaluateXpath(document, XPathConstants.STRING, String.class); - assertThat("XPath " + this.expression, result, matcher); + public void assertString(byte[] content, @Nullable String encoding, Matcher matcher) + throws Exception { + + String actual = evaluateXpath(content, encoding, String.class); + MatcherAssert.assertThat("XPath " + this.expression, actual, matcher); } /** @@ -192,9 +160,8 @@ public class XpathExpectationsHelper { * @throws Exception if content parsing or expression evaluation fails */ public void assertString(byte[] content, @Nullable String encoding, String expectedValue) throws Exception { - Document document = parseXmlByteArray(content, encoding); - String actual = evaluateXpath(document, XPathConstants.STRING, String.class); - assertEquals("XPath " + this.expression, expectedValue, actual); + String actual = evaluateXpath(content, encoding, String.class); + AssertionErrors.assertEquals("XPath " + this.expression, expectedValue, actual); } /** @@ -203,9 +170,8 @@ public class XpathExpectationsHelper { * @throws Exception if content parsing or expression evaluation fails */ public void assertNumber(byte[] content, @Nullable String encoding, Matcher matcher) throws Exception { - Document document = parseXmlByteArray(content, encoding); - Double result = evaluateXpath(document, XPathConstants.NUMBER, Double.class); - assertThat("XPath " + this.expression, result, matcher); + Double actual = evaluateXpath(content, encoding, Double.class); + MatcherAssert.assertThat("XPath " + this.expression, actual, matcher); } /** @@ -213,9 +179,8 @@ public class XpathExpectationsHelper { * @throws Exception if content parsing or expression evaluation fails */ public void assertNumber(byte[] content, @Nullable String encoding, Double expectedValue) throws Exception { - Document document = parseXmlByteArray(content, encoding); - Double actual = evaluateXpath(document, XPathConstants.NUMBER, Double.class); - assertEquals("XPath " + this.expression, expectedValue, actual); + Double actual = evaluateXpath(content, encoding, Double.class); + AssertionErrors.assertEquals("XPath " + this.expression, expectedValue, actual); } /** @@ -223,9 +188,76 @@ public class XpathExpectationsHelper { * @throws Exception if content parsing or expression evaluation fails */ public void assertBoolean(byte[] content, @Nullable String encoding, boolean expectedValue) throws Exception { + String actual = evaluateXpath(content, encoding, String.class); + AssertionErrors.assertEquals("XPath " + this.expression, expectedValue, Boolean.parseBoolean(actual)); + } + + /** + * Evaluate the XPath and return the resulting value. + * @param content the content to evaluate against + * @param encoding the encoding to use (optionally) + * @param targetClass the target class, one of Number, String, Boolean, + * org.w3c.Node, or NodeList + * @throws Exception if content parsing or expression evaluation fails + * @since 5.1 + */ + @Nullable + public T evaluateXpath(byte[] content, @Nullable String encoding, Class targetClass) throws Exception { Document document = parseXmlByteArray(content, encoding); - String actual = evaluateXpath(document, XPathConstants.STRING, String.class); - assertEquals("XPath " + this.expression, expectedValue, Boolean.parseBoolean(actual)); + return evaluateXpath(document, toQName(targetClass), targetClass); + } + + /** + * Parse the given XML content to a {@link Document}. + * @param xml the content to parse + * @param encoding optional content encoding, if provided as metadata (e.g. in HTTP headers) + * @return the parsed document + */ + protected Document parseXmlByteArray(byte[] xml, @Nullable String encoding) throws Exception { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(this.hasNamespaces); + DocumentBuilder documentBuilder = factory.newDocumentBuilder(); + InputSource inputSource = new InputSource(new ByteArrayInputStream(xml)); + if (StringUtils.hasText(encoding)) { + inputSource.setEncoding(encoding); + } + return documentBuilder.parse(inputSource); + } + + /** + * Apply the XPath expression to given document. + * @throws XPathExpressionException if expression evaluation failed + */ + @SuppressWarnings("unchecked") + @Nullable + protected T evaluateXpath(Document document, QName evaluationType, Class expectedClass) + throws XPathExpressionException { + + return (T) getXpathExpression().evaluate(document, evaluationType); + } + + private QName toQName(Class expectedClass) { + QName evaluationType; + if (Number.class.isAssignableFrom(expectedClass)) { + evaluationType = XPathConstants.NUMBER; + } + else if (CharSequence.class.isAssignableFrom(expectedClass)) { + evaluationType = XPathConstants.STRING; + } + else if (Boolean.class.isAssignableFrom(expectedClass)) { + evaluationType = XPathConstants.BOOLEAN; + } + else if (Node.class.isAssignableFrom(expectedClass)) { + evaluationType = XPathConstants.NODE; + } + else if (NodeList.class.isAssignableFrom(expectedClass)) { + evaluationType = XPathConstants.NODESET; + } + else { + throw new IllegalArgumentException("Unexpected target class " + expectedClass + ". " + + "Supported: numbers, strings, boolean, and org.w3c.Node and NodeList"); + } + return evaluationType; } } 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 87dbeec8514..3a935903bdc 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 @@ -402,6 +402,12 @@ class DefaultWebTestClient implements WebTestClient { return self(); } + @Override + public T value(Consumer consumer) { + this.result.assertWithDiagnostics(() -> consumer.accept(this.result.getResponseBody())); + return self(); + } + @Override public T consumeWith(Consumer> consumer) { this.result.assertWithDiagnostics(() -> consumer.accept(this.result)); diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/HeaderAssertions.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/HeaderAssertions.java index 98ab8e893f1..45e30a91b12 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/HeaderAssertions.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/HeaderAssertions.java @@ -17,6 +17,7 @@ package org.springframework.test.web.reactive.server; import java.util.Arrays; +import java.util.function.Consumer; import org.hamcrest.Matcher; import org.hamcrest.MatcherAssert; @@ -81,6 +82,18 @@ public class HeaderAssertions { return this.responseSpec; } + /** + * Assert the primary value of the response header with a {@link Matcher}. + * @param name the header name + * @param consumer the matcher to sue + * @since 5.1 + */ + public WebTestClient.ResponseSpec value(String name, Consumer consumer) { + String value = getRequiredValue(name); + this.exchangeResult.assertWithDiagnostics(() -> consumer.accept(value)); + return this.responseSpec; + } + private String getRequiredValue(String name) { String value = getHeaders().getFirst(name); if (value == null) { diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonPathAssertions.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonPathAssertions.java index 7222bec461d..69c6293cf6d 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonPathAssertions.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonPathAssertions.java @@ -16,6 +16,8 @@ package org.springframework.test.web.reactive.server; +import java.util.function.Consumer; + import org.hamcrest.Matcher; import org.springframework.test.util.JsonPathExpectationsHelper; @@ -152,4 +154,26 @@ public class JsonPathAssertions { return this.bodySpec; } + /** + * Consume the result of the JSONPath evaluation. + * @since 5.1 + */ + @SuppressWarnings("unchecked") + public WebTestClient.BodyContentSpec value(Consumer consumer) { + Object value = this.pathHelper.evaluateJsonPath(this.content); + consumer.accept((T) value); + return this.bodySpec; + } + + /** + * Consume the result of the JSONPath evaluation and provide a target class. + * @since 5.1 + */ + @SuppressWarnings("unchecked") + public WebTestClient.BodyContentSpec value(Consumer consumer, Class targetType) { + Object value = this.pathHelper.evaluateJsonPath(this.content, targetType); + consumer.accept((T) value); + return this.bodySpec; + } + } diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/StatusAssertions.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/StatusAssertions.java index d59f33a5cd0..3317b9dceb8 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/StatusAssertions.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/StatusAssertions.java @@ -16,6 +16,8 @@ package org.springframework.test.web.reactive.server; +import java.util.function.Consumer; + import org.hamcrest.Matcher; import org.hamcrest.MatcherAssert; @@ -208,6 +210,17 @@ public class StatusAssertions { return this.responseSpec; } + /** + * Match the response status value with a Hamcrest matcher. + * @param consumer the matcher to use + * @since 5.1 + */ + public WebTestClient.ResponseSpec value(Consumer consumer) { + int value = this.exchangeResult.getStatus().value(); + this.exchangeResult.assertWithDiagnostics(() -> consumer.accept(value)); + return this.responseSpec; + } + private WebTestClient.ResponseSpec assertStatusAndReturn(HttpStatus expected) { HttpStatus actual = this.exchangeResult.getStatus(); 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 681721485be..114bbe89c41 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 @@ -772,6 +772,12 @@ public interface WebTestClient { */ T value(Function bodyMapper, Matcher matcher); + /** + * Assert the extracted body with a {@link Matcher}. + * @since 5.1 + */ + T value(Consumer consumer); + /** * Assert the exchange result with the given {@link Consumer}. */ diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/XpathAssertions.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/XpathAssertions.java index 91b62e9320e..9b0f496292c 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/XpathAssertions.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/XpathAssertions.java @@ -19,6 +19,7 @@ package org.springframework.test.web.reactive.server; import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.Optional; +import java.util.function.Consumer; import javax.xml.xpath.XPathExpressionException; import org.hamcrest.Matcher; @@ -107,6 +108,7 @@ public class XpathAssertions { /** * Delegates to {@link XpathExpectationsHelper#assertString(byte[], String, Matcher)}. + * @since 5.1 */ public WebTestClient.BodyContentSpec string(Matcher matcher){ return assertWith(() -> this.xpathHelper.assertString(getContent(), getCharset(), matcher)); @@ -114,6 +116,7 @@ public class XpathAssertions { /** * Delegates to {@link XpathExpectationsHelper#assertNumber(byte[], String, Matcher)}. + * @since 5.1 */ public WebTestClient.BodyContentSpec number(Matcher matcher){ return assertWith(() -> this.xpathHelper.assertNumber(getContent(), getCharset(), matcher)); @@ -121,11 +124,45 @@ public class XpathAssertions { /** * Delegates to {@link XpathExpectationsHelper#assertNodeCount(byte[], String, Matcher)}. + * @since 5.1 */ public WebTestClient.BodyContentSpec nodeCount(Matcher matcher){ return assertWith(() -> this.xpathHelper.assertNodeCount(getContent(), getCharset(), matcher)); } + /** + * Consume the result of the XPath evaluation as a String. + * @since 5.1 + */ + public WebTestClient.BodyContentSpec string(Consumer consumer){ + return assertWith(() -> { + String value = this.xpathHelper.evaluateXpath(getContent(), getCharset(), String.class); + consumer.accept(value); + }); + } + + /** + * Consume the result of the XPath evaluation as a Double. + * @since 5.1 + */ + public WebTestClient.BodyContentSpec number(Consumer consumer){ + return assertWith(() -> { + Double value = this.xpathHelper.evaluateXpath(getContent(), getCharset(), Double.class); + consumer.accept(value); + }); + } + + /** + * Consume the count of nodes as result of the XPath evaluation. + * @since 5.1 + */ + public WebTestClient.BodyContentSpec nodeCount(Consumer consumer){ + return assertWith(() -> { + Integer value = this.xpathHelper.evaluateXpath(getContent(), getCharset(), Integer.class); + consumer.accept(value); + }); + } + private WebTestClient.BodyContentSpec assertWith(CheckedExceptionTask task) { try { diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ResponseEntityTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ResponseEntityTests.java index ad07f3c9561..73430239926 100644 --- a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ResponseEntityTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ResponseEntityTests.java @@ -22,6 +22,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import org.hamcrest.MatcherAssert; import org.junit.Test; import reactor.core.publisher.Flux; import reactor.test.StepVerifier; @@ -98,6 +99,18 @@ public class ResponseEntityTests { .expectBodyList(Person.class).isEqualTo(expected); } + @Test + public void entityListWithConsumer() { + + this.client.get() + .exchange() + .expectStatus().isOk() + .expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8) + .expectBodyList(Person.class).value(people -> { + MatcherAssert.assertThat(people, hasItem(new Person("Jason"))); + }); + } + @Test public void entityMap() {