Provide XML assertions in WebTestClient

Adds XML-based assertions in
org.springframework.test.web.reactive.server.WebTestClient.BodyContentSpec
for asserting against the XML body as well as xPath,
similar to those assertions offered when using
MockMvcResultMatchers.xpath().

Issue: SPR-16741
This commit is contained in:
Eric Deandrea
2018-04-18 13:53:33 -04:00
committed by Rossen Stoyanchev
parent bb5d99a083
commit 2734f01067
4 changed files with 446 additions and 0 deletions

View File

@@ -27,6 +27,7 @@ import java.util.Optional;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import java.util.function.Function;
import javax.xml.xpath.XPathExpressionException;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
@@ -40,6 +41,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.JsonExpectationsHelper;
import org.springframework.test.util.XmlExpectationsHelper;
import org.springframework.util.Assert;
import org.springframework.util.MimeType;
import org.springframework.util.MultiValueMap;
@@ -476,11 +478,34 @@ class DefaultWebTestClient implements WebTestClient {
return this;
}
@Override
public BodyContentSpec xml(String expectedXml) {
this.result.assertWithDiagnostics(() -> {
try {
new XmlExpectationsHelper().assertXmlEqual(expectedXml, getBodyAsString());
}
catch (Exception ex) {
throw new AssertionError("XML parsing error", ex);
}
});
return this;
}
@Override
public JsonPathAssertions jsonPath(String expression, Object... args) {
return new JsonPathAssertions(this, getBodyAsString(), expression, args);
}
@Override
public XpathAssertions xpath(String expression, Map<String, String> namespaces, Object... args) {
try {
return new XpathAssertions(this, expression, namespaces, args);
}
catch (XPathExpressionException ex) {
throw new AssertionError("XML parsing error", ex);
}
}
private String getBodyAsString() {
byte[] body = this.result.getResponseBody();
if (body == null || body.length == 0) {

View File

@@ -819,6 +819,14 @@ public interface WebTestClient {
*/
BodyContentSpec json(String expectedJson);
/**
* Parse the expected and actual response content as XML and perform a
* comparison verifying the same structure.
* @param expectedXml the expected JSON content.
* @since 5.1
*/
BodyContentSpec xml(String expectedXml);
/**
* Access to response body assertions using a
* <a href="https://github.com/jayway/JsonPath">JsonPath</a> expression
@@ -830,6 +838,32 @@ 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.
* <p>The XPath expression can be a parameterized string using
* formatting specifiers as defined in {@link String#format}.
* @param expression The XPath expression
* @param args arguments to parameterize the expression
* @see #xpath(String, Map, Object...)
* @since 5.1
*/
default XpathAssertions xpath(String expression, Object... args){
return xpath(expression, null, args);
}
/**
* Access to response body assertions with specific namespaces using an XPath
* expression to inspect a specific subset of the body.
* <p>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 args arguments to parameterize the expression
* @since 5.1
*/
XpathAssertions xpath(String expression, Map<String, String> namespaces, Object... args);
/**
* Assert the response body content with the given {@link Consumer}.
* @param consumer the consumer for the response body; the input

View File

@@ -0,0 +1,159 @@
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.web.reactive.server;
import java.nio.charset.StandardCharsets;
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.MimeType;
/**
* XPath assertions for a {@link WebTestClient}.
*
* @author Eric Deandrea
* @since 5.1
*/
public class XpathAssertions {
private final WebTestClient.BodyContentSpec bodySpec;
private final XpathExpectationsHelper xpathHelper;
XpathAssertions(WebTestClient.BodyContentSpec spec, String expression, @Nullable Map<String, String> namespaces, Object... args) throws XPathExpressionException {
this.bodySpec = spec;
this.xpathHelper = new XpathExpectationsHelper(expression, namespaces, args);
}
/**
* Applies {@link XpathExpectationsHelper#assertString(byte[], String, String)}
*/
public WebTestClient.BodyContentSpec isEqualTo(String expectedValue) {
return performXmlAssertionAndHandleError(() -> this.xpathHelper.assertString(getResponseBody(), getDefinedEncoding(), expectedValue));
}
/**
* Applies {@link XpathExpectationsHelper#assertNumber(byte[], String, Double)}
*/
public WebTestClient.BodyContentSpec isEqualTo(Double expectedValue) {
return performXmlAssertionAndHandleError(() -> this.xpathHelper.assertNumber(getResponseBody(), getDefinedEncoding(), expectedValue));
}
/**
* Applies {@link XpathExpectationsHelper#assertBoolean(byte[], String, boolean)}
*/
public WebTestClient.BodyContentSpec isEqualTo(boolean expectedValue) {
return performXmlAssertionAndHandleError(() -> this.xpathHelper.assertBoolean(getResponseBody(), getDefinedEncoding(), expectedValue));
}
/**
* Applies {@link XpathExpectationsHelper#exists(byte[], String)}
*/
public WebTestClient.BodyContentSpec exists() {
return performXmlAssertionAndHandleError(() -> this.xpathHelper.exists(getResponseBody(), getDefinedEncoding()));
}
/**
* Applies {@link XpathExpectationsHelper#doesNotExist(byte[], String)}
*/
public WebTestClient.BodyContentSpec doesNotExist() {
return performXmlAssertionAndHandleError(() -> this.xpathHelper.doesNotExist(getResponseBody(), getDefinedEncoding()));
}
/**
* Applies {@link XpathExpectationsHelper[#assertNodeCount(byte[], String, int)}
*/
public WebTestClient.BodyContentSpec nodeCount(int expectedCount) {
return performXmlAssertionAndHandleError(() -> this.xpathHelper.assertNodeCount(getResponseBody(), getDefinedEncoding(), expectedCount));
}
/**
* Applies {@link XpathExpectationsHelper#assertNodeCount(byte[], String, Matcher)}
*/
public WebTestClient.BodyContentSpec nodeCount(Matcher<Integer> matcher) {
return performXmlAssertionAndHandleError(() -> this.xpathHelper.assertNodeCount(getResponseBody(), getDefinedEncoding(), matcher));
}
/**
* Applies {@link XpathExpectationsHelper#assertNode(byte[], String, Matcher)}
*/
public WebTestClient.BodyContentSpec nodeMatches(Matcher<? super Node> matcher) {
return performXmlAssertionAndHandleError(() -> this.xpathHelper.assertNode(getResponseBody(), getDefinedEncoding(), matcher));
}
/**
* 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;
}
private byte[] getResponseBody() {
return getResult().getResponseBody();
}
private EntityExchangeResult<byte[]> getResult() {
return this.bodySpec.returnResult();
}
private String getDefinedEncoding() {
return Optional.ofNullable(getResult())
.map(EntityExchangeResult::getResponseHeaders)
.map(HttpHeaders::getContentType)
.map(MimeType::getCharset)
.orElse(StandardCharsets.UTF_8)
.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;
@Override
default void run() {
try {
runThrows();
}
catch (Exception ex) {
throw new AssertionError("XML parsing error", ex);
}
}
}
}

View File

@@ -0,0 +1,228 @@
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.web.reactive.server.samples;
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;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Samples of tests using {@link WebTestClient} with serialized XML content.
*
* @author Eric Deandrea
* @since 5.1
*/
public class XmlContentTests {
private static final String PEOPLE_XML =
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"
+ "<people><people>"
+ "<person><name>Jane</name></person>"
+ "<person><name>Jason</name></person>"
+ "<person><name>John</name></person>"
+ "</people></people>";
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);
}
@Test
public void xpathIsEqualTo() {
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("/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"));
}
@Test
public void xpathContainsSubstringViaRegex() {
this.client.get().uri("/persons/John")
.accept(MediaType.APPLICATION_XML)
.exchange()
.expectStatus().isOk()
.expectBody()
.xpath("//name[contains(text(), 'oh')]").exists();
}
@Test
public void postXmlContent() {
this.client.post().uri("/persons")
.contentType(MediaType.APPLICATION_XML)
.syncBody("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><person><name>John</name></person>")
.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")
@XmlAccessorType(XmlAccessType.FIELD)
private static class PeopleWrapper {
@XmlElementWrapper(name="people")
@XmlElement(name="person")
private final List<Person> people = new ArrayList<>();
public PeopleWrapper() {
}
public PeopleWrapper(List<Person> people) {
this.people.addAll(people);
}
public PeopleWrapper(Person... people) {
this.people.addAll(Arrays.asList(people));
}
public List<Person> getPeople() {
return this.people;
}
}
@RestController
@RequestMapping("/persons")
static class PersonController {
@GetMapping(produces = MediaType.APPLICATION_XML_VALUE)
Mono<PeopleWrapper> getPersons() {
return Mono.just(new PeopleWrapper(new Person("Jane"), new Person("Jason"), new Person("John")));
}
@GetMapping(path = "/{name}", produces = MediaType.APPLICATION_XML_VALUE)
Mono<Person> getPerson(@PathVariable String name) {
return Mono.just(new Person(name));
}
@PostMapping(consumes = MediaType.APPLICATION_XML_VALUE)
Mono<ResponseEntity<Object>> savePeople(@RequestBody Flux<Person> person) {
return person
.map(Person::getName)
.map(name -> String.format("/persons/%s", name))
.map(URI::create)
.map(ResponseEntity::created)
.map(ResponseEntity.BodyBuilder::build)
.next();
}
}
}