Leaving out hamcrest for now, to address more broadly.

Issue: SPR-16741
This commit is contained in:
Rossen Stoyanchev 2018-07-20 10:36:57 -04:00
parent 2734f01067
commit 0c62d6b5da
5 changed files with 117 additions and 191 deletions

View File

@ -497,13 +497,8 @@ class DefaultWebTestClient implements WebTestClient {
} }
@Override @Override
public XpathAssertions xpath(String expression, Map<String, String> namespaces, Object... args) { public XpathAssertions xpath(String expression, @Nullable Map<String, String> namespaces, Object... args) {
try { return new XpathAssertions(this, expression, namespaces, args);
return new XpathAssertions(this, expression, namespaces, args);
}
catch (XPathExpressionException ex) {
throw new AssertionError("XML parsing error", ex);
}
} }
private String getBodyAsString() { private String getBodyAsString() {

View File

@ -36,6 +36,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.ClientHttpRequest; import org.springframework.http.client.reactive.ClientHttpRequest;
import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.lang.Nullable;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
import org.springframework.validation.Validator; import org.springframework.validation.Validator;
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder; import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
@ -820,10 +821,15 @@ public interface WebTestClient {
BodyContentSpec json(String expectedJson); BodyContentSpec json(String expectedJson);
/** /**
* Parse the expected and actual response content as XML and perform a * Parse expected and actual response content as XML and assert that
* comparison verifying the same structure. * the two are "similar", i.e. they contain the same elements and
* attributes regardless of order.
* <p>Use of this method requires the
* <a href="https://github.com/xmlunit/xmlunit">XMLUnit</a> library on
* the classpath.
* @param expectedXml the expected JSON content. * @param expectedXml the expected JSON content.
* @since 5.1 * @since 5.1
* @see org.springframework.test.util.XmlExpectationsHelper#assertXmlEqual(String, String)
*/ */
BodyContentSpec xml(String expectedXml); BodyContentSpec xml(String expectedXml);
@ -839,11 +845,11 @@ public interface WebTestClient {
JsonPathAssertions jsonPath(String expression, Object... args); JsonPathAssertions jsonPath(String expression, Object... args);
/** /**
* Access to response body assertions using an XPath expression to inspect a specific * Access to response body assertions using an XPath expression to
* subset of the body. * inspect a specific subset of the body.
* <p>The XPath expression can be a parameterized string using * <p>The XPath expression can be a parameterized string using
* formatting specifiers as defined in {@link String#format}. * 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 * @param args arguments to parameterize the expression
* @see #xpath(String, Map, Object...) * @see #xpath(String, Map, Object...)
* @since 5.1 * @since 5.1
@ -853,16 +859,16 @@ public interface WebTestClient {
} }
/** /**
* Access to response body assertions with specific namespaces using an XPath * Access to response body assertions with specific namespaces using an
* expression to inspect a specific subset of the body. * XPath expression to inspect a specific subset of the body.
* <p>The XPath expression can be a parameterized string using * <p>The XPath expression can be a parameterized string using
* formatting specifiers as defined in {@link String#format}. * formatting specifiers as defined in {@link String#format}.
* @param expression The XPath expression * @param expression the XPath expression
* @param namespaces The namespaces * @param namespaces namespaces to use
* @param args arguments to parameterize the expression * @param args arguments to parameterize the expression
* @since 5.1 * @since 5.1
*/ */
XpathAssertions xpath(String expression, Map<String, String> namespaces, Object... args); XpathAssertions xpath(String expression, @Nullable Map<String, String> namespaces, Object... args);
/** /**
* Assert the response body content with the given {@link Consumer}. * Assert the response body content with the given {@link Consumer}.

View File

@ -21,18 +21,18 @@ import java.util.Map;
import java.util.Optional; import java.util.Optional;
import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathExpressionException;
import org.hamcrest.Matcher;
import org.w3c.dom.Node;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.test.util.XpathExpectationsHelper; import org.springframework.test.util.XpathExpectationsHelper;
import org.springframework.util.Assert;
import org.springframework.util.MimeType; import org.springframework.util.MimeType;
/** /**
* XPath assertions for a {@link WebTestClient}. * XPath assertions for the {@link WebTestClient}.
* *
* @author Eric Deandrea * @author Eric Deandrea
* @author Rossen Stoyanchev
* @since 5.1 * @since 5.1
*/ */
public class XpathAssertions { public class XpathAssertions {
@ -41,96 +41,87 @@ public class XpathAssertions {
private final XpathExpectationsHelper xpathHelper; private final XpathExpectationsHelper xpathHelper;
XpathAssertions(WebTestClient.BodyContentSpec spec, String expression, @Nullable Map<String, String> namespaces, Object... args) throws XPathExpressionException {
XpathAssertions(WebTestClient.BodyContentSpec spec,
String expression, @Nullable Map<String, String> namespaces, Object... args) {
this.bodySpec = spec; this.bodySpec = spec;
this.xpathHelper = new XpathExpectationsHelper(expression, namespaces, args); this.xpathHelper = initXpathHelper(expression, namespaces, args);
} }
private static XpathExpectationsHelper initXpathHelper(
String expression, @Nullable Map<String, String> 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) { 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) { 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) { 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() { 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() { 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) { 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<Integer> matcher) {
return performXmlAssertionAndHandleError(() -> this.xpathHelper.assertNodeCount(getResponseBody(), getDefinedEncoding(), matcher));
}
/** private WebTestClient.BodyContentSpec assertWith(CheckedExceptionTask task) {
* Applies {@link XpathExpectationsHelper#assertNode(byte[], String, Matcher)} try {
*/ task.run();
public WebTestClient.BodyContentSpec nodeMatches(Matcher<? super Node> matcher) { }
return performXmlAssertionAndHandleError(() -> this.xpathHelper.assertNode(getResponseBody(), getDefinedEncoding(), matcher)); catch (Exception ex) {
} throw new AssertionError("XML parsing error", ex);
}
/**
* Applies {@link XpathExpectationsHelper#assertString(byte[], String, Matcher)}
*/
public WebTestClient.BodyContentSpec matchesString(Matcher<? super String> matcher) {
return performXmlAssertionAndHandleError(() -> this.xpathHelper.assertString(getResponseBody(), getDefinedEncoding(), matcher));
}
/**
* Applies {@link XpathExpectationsHelper#assertNumber(byte[], String, Matcher)}
*/
public WebTestClient.BodyContentSpec matchesNumber(Matcher<? super Double> matcher) {
return performXmlAssertionAndHandleError(() -> this.xpathHelper.assertNumber(getResponseBody(), getDefinedEncoding(), matcher));
}
private WebTestClient.BodyContentSpec performXmlAssertionAndHandleError(AssertionThrowingRunnable assertion) {
assertion.run();
return this.bodySpec; return this.bodySpec;
} }
private byte[] getResponseBody() { private byte[] getContent() {
return getResult().getResponseBody(); byte[] body = this.bodySpec.returnResult().getResponseBody();
Assert.notNull(body, "Expected body content");
return body;
} }
private EntityExchangeResult<byte[]> getResult() { private String getCharset() {
return this.bodySpec.returnResult(); return Optional.of(this.bodySpec.returnResult())
}
private String getDefinedEncoding() {
return Optional.ofNullable(getResult())
.map(EntityExchangeResult::getResponseHeaders) .map(EntityExchangeResult::getResponseHeaders)
.map(HttpHeaders::getContentType) .map(HttpHeaders::getContentType)
.map(MimeType::getCharset) .map(MimeType::getCharset)
@ -138,22 +129,14 @@ public class XpathAssertions {
.name(); .name();
} }
/** /**
* Lets us be able to use lambda expressions that could throw checked exceptions, since * Lets us be able to use lambda expressions that could throw checked exceptions, since
* {@link XpathExpectationsHelper} throws {@link Exception} on its methods. * {@link XpathExpectationsHelper} throws {@link Exception} on its methods.
*/ */
@FunctionalInterface private interface CheckedExceptionTask {
private interface AssertionThrowingRunnable extends Runnable {
void runThrows() throws Exception; void run() throws Exception;
@Override
default void run() {
try {
runThrows();
}
catch (Exception ex) {
throw new AssertionError("XML parsing error", ex);
}
}
} }
} }

View File

@ -15,18 +15,30 @@
*/ */
package org.springframework.test.web.reactive.server.samples; 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.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
@XmlRootElement
class Person { class Person {
private final String name; private String name;
// No-arg constructor for XML
public Person() {
}
@JsonCreator @JsonCreator
public Person(@JsonProperty("name") String name) { public Person(@JsonProperty("name") String name) {
this.name = name; this.name = name;
} }
public void setName(String name) {
this.name = name;
}
public String getName() { public String getName() {
return this.name; return this.name;
} }

View File

@ -20,24 +20,17 @@ import java.net.URI;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import javax.validation.constraints.NotNull;
import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlRootElement;
import org.hamcrest.Matchers;
import org.junit.Test; import org.junit.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.test.web.reactive.server.WebTestClient; 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.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; 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; 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 * @author Eric Deandrea
* @since 5.1 * @since 5.1
*/ */
public class XmlContentTests { public class XmlContentTests {
private static final String PEOPLE_XML = private static final String persons_XML =
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>" "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"
+ "<people><people>" + "<persons>"
+ "<person><name>Jane</name></person>" + "<person><name>Jane</name></person>"
+ "<person><name>Jason</name></person>" + "<person><name>Jason</name></person>"
+ "<person><name>John</name></person>" + "<person><name>John</name></person>"
+ "</people></people>"; + "</persons>";
private final WebTestClient client = WebTestClient.bindToController(new PersonController()).build(); private final WebTestClient client = WebTestClient.bindToController(new PersonController()).build();
@Test @Test
public void xmlContent() { public void xmlContent() {
this.client.get().uri("/persons") this.client.get().uri("/persons")
.accept(MediaType.APPLICATION_XML) .accept(MediaType.APPLICATION_XML)
.exchange() .exchange()
.expectStatus().isOk() .expectStatus().isOk()
.expectBody().xml(PEOPLE_XML); .expectBody().xml(persons_XML);
} }
@Test @Test
@ -80,28 +75,12 @@ public class XmlContentTests {
.expectStatus().isOk() .expectStatus().isOk()
.expectBody() .expectBody()
.xpath("/").exists() .xpath("/").exists()
.xpath("/people").exists() .xpath("/persons").exists()
.xpath("/people/people").exists() .xpath("/persons/person").exists()
.xpath("/people/people/person").exists() .xpath("/persons/person").nodeCount(3)
.xpath("/people/people/person").nodeCount(3) .xpath("/persons/person[1]/name").isEqualTo("Jane")
.xpath("/people/people/person[1]/name").isEqualTo("Jane") .xpath("/persons/person[2]/name").isEqualTo("Jason")
.xpath("/people/people/person[2]/name").isEqualTo("Jason") .xpath("/persons/person[3]/name").isEqualTo("John");
.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"));
} }
@Test @Test
@ -116,86 +95,42 @@ public class XmlContentTests {
@Test @Test
public void postXmlContent() { public void postXmlContent() {
String content =
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>" +
"<person><name>John</name></person>";
this.client.post().uri("/persons") this.client.post().uri("/persons")
.contentType(MediaType.APPLICATION_XML) .contentType(MediaType.APPLICATION_XML)
.syncBody("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><person><name>John</name></person>") .syncBody(content)
.exchange() .exchange()
.expectStatus().isCreated() .expectStatus().isCreated()
.expectHeader().valueEquals(HttpHeaders.LOCATION, "/persons/John") .expectHeader().valueEquals(HttpHeaders.LOCATION, "/persons/John")
.expectBody().isEmpty(); .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") @SuppressWarnings("unused")
@XmlRootElement(name="people") @XmlRootElement(name="persons")
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)
private static class PeopleWrapper { private static class PersonsWrapper {
@XmlElementWrapper(name="people")
@XmlElement(name="person") @XmlElement(name="person")
private final List<Person> people = new ArrayList<>(); private final List<Person> persons = new ArrayList<>();
public PeopleWrapper() { public PersonsWrapper() {
} }
public PeopleWrapper(List<Person> people) { public PersonsWrapper(List<Person> persons) {
this.people.addAll(people); this.persons.addAll(persons);
} }
public PeopleWrapper(Person... people) { public PersonsWrapper(Person... persons) {
this.people.addAll(Arrays.asList(people)); this.persons.addAll(Arrays.asList(persons));
} }
public List<Person> getPeople() { public List<Person> getpersons() {
return this.people; return this.persons;
} }
} }
@ -204,24 +139,19 @@ public class XmlContentTests {
static class PersonController { static class PersonController {
@GetMapping(produces = MediaType.APPLICATION_XML_VALUE) @GetMapping(produces = MediaType.APPLICATION_XML_VALUE)
Mono<PeopleWrapper> getPersons() { PersonsWrapper getPersons() {
return Mono.just(new PeopleWrapper(new Person("Jane"), new Person("Jason"), new Person("John"))); return new PersonsWrapper(new Person("Jane"), new Person("Jason"), new Person("John"));
} }
@GetMapping(path = "/{name}", produces = MediaType.APPLICATION_XML_VALUE) @GetMapping(path = "/{name}", produces = MediaType.APPLICATION_XML_VALUE)
Mono<Person> getPerson(@PathVariable String name) { Person getPerson(@PathVariable String name) {
return Mono.just(new Person(name)); return new Person(name);
} }
@PostMapping(consumes = MediaType.APPLICATION_XML_VALUE) @PostMapping(consumes = MediaType.APPLICATION_XML_VALUE)
Mono<ResponseEntity<Object>> savePeople(@RequestBody Flux<Person> person) { ResponseEntity<Object> savepersons(@RequestBody Person person) {
return person URI location = URI.create(String.format("/persons/%s", person.getName()));
.map(Person::getName) return ResponseEntity.created(location).build();
.map(name -> String.format("/persons/%s", name))
.map(URI::create)
.map(ResponseEntity::created)
.map(ResponseEntity.BodyBuilder::build)
.next();
} }
} }