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 30c66b846bd..e5bb31efaae 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 @@ -497,13 +497,8 @@ class DefaultWebTestClient implements WebTestClient { } @Override - public XpathAssertions xpath(String expression, Map namespaces, Object... args) { - try { - return new XpathAssertions(this, expression, namespaces, args); - } - catch (XPathExpressionException ex) { - throw new AssertionError("XML parsing error", ex); - } + public XpathAssertions xpath(String expression, @Nullable Map namespaces, Object... args) { + return new XpathAssertions(this, expression, namespaces, args); } private String getBodyAsString() { 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 fd8ca30c9ff..90502e7ed5e 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 @@ -36,6 +36,7 @@ import org.springframework.http.MediaType; import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.http.client.reactive.ClientHttpRequest; import org.springframework.http.codec.ServerCodecConfigurer; +import org.springframework.lang.Nullable; import org.springframework.util.MultiValueMap; import org.springframework.validation.Validator; import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder; @@ -820,10 +821,15 @@ public interface WebTestClient { BodyContentSpec json(String expectedJson); /** - * Parse the expected and actual response content as XML and perform a - * comparison verifying the same structure. + * Parse expected and actual response content as XML and assert that + * the two are "similar", i.e. they contain the same elements and + * attributes regardless of order. + *

Use of this method requires the + * XMLUnit library on + * the classpath. * @param expectedXml the expected JSON content. * @since 5.1 + * @see org.springframework.test.util.XmlExpectationsHelper#assertXmlEqual(String, String) */ BodyContentSpec xml(String expectedXml); @@ -839,11 +845,11 @@ public interface WebTestClient { JsonPathAssertions jsonPath(String expression, Object... args); /** - * Access to response body assertions using an XPath expression to inspect a specific - * subset of the body. + * Access to response body assertions using an XPath expression to + * inspect a specific subset of the body. *

The XPath expression can be a parameterized string using * formatting specifiers as defined in {@link String#format}. - * @param expression The XPath expression + * @param expression the XPath expression * @param args arguments to parameterize the expression * @see #xpath(String, Map, Object...) * @since 5.1 @@ -853,16 +859,16 @@ public interface WebTestClient { } /** - * Access to response body assertions with specific namespaces using an XPath - * expression to inspect a specific subset of the body. + * Access to response body assertions with specific namespaces using an + * XPath expression to inspect a specific subset of the body. *

The XPath expression can be a parameterized string using * formatting specifiers as defined in {@link String#format}. - * @param expression The XPath expression - * @param namespaces The namespaces + * @param expression the XPath expression + * @param namespaces namespaces to use * @param args arguments to parameterize the expression * @since 5.1 */ - XpathAssertions xpath(String expression, Map namespaces, Object... args); + XpathAssertions xpath(String expression, @Nullable Map namespaces, Object... args); /** * Assert the response body content 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 c3d966b5f42..a882d9ddc2c 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 @@ -21,18 +21,18 @@ import java.util.Map; import java.util.Optional; import javax.xml.xpath.XPathExpressionException; -import org.hamcrest.Matcher; -import org.w3c.dom.Node; - import org.springframework.http.HttpHeaders; import org.springframework.lang.Nullable; import org.springframework.test.util.XpathExpectationsHelper; +import org.springframework.util.Assert; import org.springframework.util.MimeType; + /** - * XPath assertions for a {@link WebTestClient}. + * XPath assertions for the {@link WebTestClient}. * * @author Eric Deandrea + * @author Rossen Stoyanchev * @since 5.1 */ public class XpathAssertions { @@ -41,96 +41,87 @@ public class XpathAssertions { private final XpathExpectationsHelper xpathHelper; - XpathAssertions(WebTestClient.BodyContentSpec spec, String expression, @Nullable Map namespaces, Object... args) throws XPathExpressionException { + + XpathAssertions(WebTestClient.BodyContentSpec spec, + String expression, @Nullable Map namespaces, Object... args) { + this.bodySpec = spec; - this.xpathHelper = new XpathExpectationsHelper(expression, namespaces, args); + this.xpathHelper = initXpathHelper(expression, namespaces, args); } + private static XpathExpectationsHelper initXpathHelper( + String expression, @Nullable Map namespaces, Object[] args) { + + try { + return new XpathExpectationsHelper(expression, namespaces, args); + } + catch (XPathExpressionException ex) { + throw new AssertionError("XML parsing error", ex); + } + } + + /** - * Applies {@link XpathExpectationsHelper#assertString(byte[], String, String)} + * Delegates to {@link XpathExpectationsHelper#assertString(byte[], String, String)} */ public WebTestClient.BodyContentSpec isEqualTo(String expectedValue) { - return performXmlAssertionAndHandleError(() -> this.xpathHelper.assertString(getResponseBody(), getDefinedEncoding(), expectedValue)); + return assertWith(() -> this.xpathHelper.assertString(getContent(), getCharset(), expectedValue)); } /** - * Applies {@link XpathExpectationsHelper#assertNumber(byte[], String, Double)} + * Delegates to {@link XpathExpectationsHelper#assertNumber(byte[], String, Double)} */ public WebTestClient.BodyContentSpec isEqualTo(Double expectedValue) { - return performXmlAssertionAndHandleError(() -> this.xpathHelper.assertNumber(getResponseBody(), getDefinedEncoding(), expectedValue)); + return assertWith(() -> this.xpathHelper.assertNumber(getContent(), getCharset(), expectedValue)); } /** - * Applies {@link XpathExpectationsHelper#assertBoolean(byte[], String, boolean)} + * Delegates to {@link XpathExpectationsHelper#assertBoolean(byte[], String, boolean)} */ public WebTestClient.BodyContentSpec isEqualTo(boolean expectedValue) { - return performXmlAssertionAndHandleError(() -> this.xpathHelper.assertBoolean(getResponseBody(), getDefinedEncoding(), expectedValue)); + return assertWith(() -> this.xpathHelper.assertBoolean(getContent(), getCharset(), expectedValue)); } /** - * Applies {@link XpathExpectationsHelper#exists(byte[], String)} + * Delegates to {@link XpathExpectationsHelper#exists(byte[], String)} */ public WebTestClient.BodyContentSpec exists() { - return performXmlAssertionAndHandleError(() -> this.xpathHelper.exists(getResponseBody(), getDefinedEncoding())); + return assertWith(() -> this.xpathHelper.exists(getContent(), getCharset())); } /** - * Applies {@link XpathExpectationsHelper#doesNotExist(byte[], String)} + * Delegates to {@link XpathExpectationsHelper#doesNotExist(byte[], String)} */ public WebTestClient.BodyContentSpec doesNotExist() { - return performXmlAssertionAndHandleError(() -> this.xpathHelper.doesNotExist(getResponseBody(), getDefinedEncoding())); + return assertWith(() -> this.xpathHelper.doesNotExist(getContent(), getCharset())); } /** - * Applies {@link XpathExpectationsHelper[#assertNodeCount(byte[], String, int)} + * Delegates to {@link XpathExpectationsHelper[#assertNodeCount(byte[], String, int)} */ public WebTestClient.BodyContentSpec nodeCount(int expectedCount) { - return performXmlAssertionAndHandleError(() -> this.xpathHelper.assertNodeCount(getResponseBody(), getDefinedEncoding(), expectedCount)); + return assertWith(() -> this.xpathHelper.assertNodeCount(getContent(), getCharset(), expectedCount)); } - /** - * Applies {@link XpathExpectationsHelper#assertNodeCount(byte[], String, Matcher)} - */ - public WebTestClient.BodyContentSpec nodeCount(Matcher matcher) { - return performXmlAssertionAndHandleError(() -> this.xpathHelper.assertNodeCount(getResponseBody(), getDefinedEncoding(), matcher)); - } - /** - * Applies {@link XpathExpectationsHelper#assertNode(byte[], String, Matcher)} - */ - public WebTestClient.BodyContentSpec nodeMatches(Matcher matcher) { - return performXmlAssertionAndHandleError(() -> this.xpathHelper.assertNode(getResponseBody(), getDefinedEncoding(), matcher)); - } - - /** - * Applies {@link XpathExpectationsHelper#assertString(byte[], String, Matcher)} - */ - public WebTestClient.BodyContentSpec matchesString(Matcher matcher) { - return performXmlAssertionAndHandleError(() -> this.xpathHelper.assertString(getResponseBody(), getDefinedEncoding(), matcher)); - } - - /** - * Applies {@link XpathExpectationsHelper#assertNumber(byte[], String, Matcher)} - */ - public WebTestClient.BodyContentSpec matchesNumber(Matcher matcher) { - return performXmlAssertionAndHandleError(() -> this.xpathHelper.assertNumber(getResponseBody(), getDefinedEncoding(), matcher)); - } - - private WebTestClient.BodyContentSpec performXmlAssertionAndHandleError(AssertionThrowingRunnable assertion) { - assertion.run(); + private WebTestClient.BodyContentSpec assertWith(CheckedExceptionTask task) { + try { + task.run(); + } + catch (Exception ex) { + throw new AssertionError("XML parsing error", ex); + } return this.bodySpec; } - private byte[] getResponseBody() { - return getResult().getResponseBody(); + private byte[] getContent() { + byte[] body = this.bodySpec.returnResult().getResponseBody(); + Assert.notNull(body, "Expected body content"); + return body; } - private EntityExchangeResult getResult() { - return this.bodySpec.returnResult(); - } - - private String getDefinedEncoding() { - return Optional.ofNullable(getResult()) + private String getCharset() { + return Optional.of(this.bodySpec.returnResult()) .map(EntityExchangeResult::getResponseHeaders) .map(HttpHeaders::getContentType) .map(MimeType::getCharset) @@ -138,22 +129,14 @@ public class XpathAssertions { .name(); } + /** * Lets us be able to use lambda expressions that could throw checked exceptions, since * {@link XpathExpectationsHelper} throws {@link Exception} on its methods. */ - @FunctionalInterface - private interface AssertionThrowingRunnable extends Runnable { - void runThrows() throws Exception; + private interface CheckedExceptionTask { + + void run() throws Exception; - @Override - default void run() { - try { - runThrows(); - } - catch (Exception ex) { - throw new AssertionError("XML parsing error", ex); - } - } } } \ No newline at end of file diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/Person.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/Person.java index ffe5bfa14ad..4aceffea46d 100644 --- a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/Person.java +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/Person.java @@ -15,18 +15,30 @@ */ package org.springframework.test.web.reactive.server.samples; +import javax.xml.bind.annotation.XmlRootElement; + import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +@XmlRootElement class Person { - private final String name; + private String name; + + + // No-arg constructor for XML + public Person() { + } @JsonCreator public Person(@JsonProperty("name") String name) { this.name = name; } + public void setName(String name) { + this.name = name; + } + public String getName() { return this.name; } diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/XmlContentTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/XmlContentTests.java index 10abc2305f4..99ea60afba1 100644 --- a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/XmlContentTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/XmlContentTests.java @@ -20,24 +20,17 @@ import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.List; - -import javax.validation.constraints.NotNull; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlElementWrapper; import javax.xml.bind.annotation.XmlRootElement; -import org.hamcrest.Matchers; import org.junit.Test; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.test.web.reactive.server.WebTestClient; -import org.springframework.util.ObjectUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -46,30 +39,32 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** - * Samples of tests using {@link WebTestClient} with serialized XML content. + * Samples of tests using {@link WebTestClient} with XML content. * * @author Eric Deandrea * @since 5.1 */ public class XmlContentTests { - private static final String PEOPLE_XML = + private static final String persons_XML = "" - + "" + + "" + "Jane" + "Jason" + "John" - + ""; + + ""; + private final WebTestClient client = WebTestClient.bindToController(new PersonController()).build(); + @Test public void xmlContent() { this.client.get().uri("/persons") .accept(MediaType.APPLICATION_XML) .exchange() .expectStatus().isOk() - .expectBody().xml(PEOPLE_XML); + .expectBody().xml(persons_XML); } @Test @@ -80,28 +75,12 @@ public class XmlContentTests { .expectStatus().isOk() .expectBody() .xpath("/").exists() - .xpath("/people").exists() - .xpath("/people/people").exists() - .xpath("/people/people/person").exists() - .xpath("/people/people/person").nodeCount(3) - .xpath("/people/people/person[1]/name").isEqualTo("Jane") - .xpath("/people/people/person[2]/name").isEqualTo("Jason") - .xpath("/people/people/person[3]/name").isEqualTo("John"); - } - - @Test - public void xpathMatches() { - this.client.get().uri("/persons") - .accept(MediaType.APPLICATION_XML) - .exchange() - .expectStatus().isOk() - .expectBody() - .xpath("/").exists() - .xpath("/people").exists() - .xpath("/people/people").exists() - .xpath("/people/people/person").exists() - .xpath("/people/people/person").nodeCount(3) - .xpath("//person/name").matchesString(Matchers.startsWith("J")); + .xpath("/persons").exists() + .xpath("/persons/person").exists() + .xpath("/persons/person").nodeCount(3) + .xpath("/persons/person[1]/name").isEqualTo("Jane") + .xpath("/persons/person[2]/name").isEqualTo("Jason") + .xpath("/persons/person[3]/name").isEqualTo("John"); } @Test @@ -116,86 +95,42 @@ public class XmlContentTests { @Test public void postXmlContent() { + + String content = + "" + + "John"; + this.client.post().uri("/persons") .contentType(MediaType.APPLICATION_XML) - .syncBody("John") + .syncBody(content) .exchange() .expectStatus().isCreated() .expectHeader().valueEquals(HttpHeaders.LOCATION, "/persons/John") .expectBody().isEmpty(); } - @XmlRootElement - private static class Person { - - @NotNull - private String name; - - public Person() { - } - - public Person(String name) { - this.name = name; - } - - public String getName() { - return this.name; - } - - public void setName(String name) { - this.name = name; - } - - public Person name(String name) { - setName(name); - return this; - } - - @Override - public boolean equals(Object other) { - if (this == other) { - return true; - } - if (!(other instanceof Person)) { - return false; - } - Person otherPerson = (Person) other; - return ObjectUtils.nullSafeEquals(this.name, otherPerson.name); - } - - @Override - public int hashCode() { - return Person.class.hashCode(); - } - - @Override - public String toString() { - return "Person [name=" + this.name + "]"; - } - } @SuppressWarnings("unused") - @XmlRootElement(name="people") + @XmlRootElement(name="persons") @XmlAccessorType(XmlAccessType.FIELD) - private static class PeopleWrapper { + private static class PersonsWrapper { - @XmlElementWrapper(name="people") @XmlElement(name="person") - private final List people = new ArrayList<>(); + private final List persons = new ArrayList<>(); - public PeopleWrapper() { + public PersonsWrapper() { } - public PeopleWrapper(List people) { - this.people.addAll(people); + public PersonsWrapper(List persons) { + this.persons.addAll(persons); } - public PeopleWrapper(Person... people) { - this.people.addAll(Arrays.asList(people)); + public PersonsWrapper(Person... persons) { + this.persons.addAll(Arrays.asList(persons)); } - public List getPeople() { - return this.people; + public List getpersons() { + return this.persons; } } @@ -204,24 +139,19 @@ public class XmlContentTests { static class PersonController { @GetMapping(produces = MediaType.APPLICATION_XML_VALUE) - Mono getPersons() { - return Mono.just(new PeopleWrapper(new Person("Jane"), new Person("Jason"), new Person("John"))); + PersonsWrapper getPersons() { + return new PersonsWrapper(new Person("Jane"), new Person("Jason"), new Person("John")); } @GetMapping(path = "/{name}", produces = MediaType.APPLICATION_XML_VALUE) - Mono getPerson(@PathVariable String name) { - return Mono.just(new Person(name)); + Person getPerson(@PathVariable String name) { + return new Person(name); } @PostMapping(consumes = MediaType.APPLICATION_XML_VALUE) - Mono> savePeople(@RequestBody Flux person) { - return person - .map(Person::getName) - .map(name -> String.format("/persons/%s", name)) - .map(URI::create) - .map(ResponseEntity::created) - .map(ResponseEntity.BodyBuilder::build) - .next(); + ResponseEntity savepersons(@RequestBody Person person) { + URI location = URI.create(String.format("/persons/%s", person.getName())); + return ResponseEntity.created(location).build(); } }