Hamcrest methods in WebTestClient

Issue: SPR-16729
This commit is contained in:
Rossen Stoyanchev 2018-07-20 17:37:12 -04:00
parent 0c62d6b5da
commit 20de5003ff
11 changed files with 210 additions and 55 deletions

View File

@ -18,6 +18,7 @@ package org.springframework.test.web.reactive.server;
import java.net.URI;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.Arrays;
@ -27,8 +28,9 @@ 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.hamcrest.Matcher;
import org.hamcrest.MatcherAssert;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
@ -40,6 +42,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.ClientHttpRequest;
import org.springframework.lang.Nullable;
import org.springframework.test.util.AssertionErrors;
import org.springframework.test.util.JsonExpectationsHelper;
import org.springframework.test.util.XmlExpectationsHelper;
import org.springframework.util.Assert;
@ -50,10 +53,6 @@ import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.UriBuilder;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.springframework.test.util.AssertionErrors.assertEquals;
import static org.springframework.test.util.AssertionErrors.assertTrue;
/**
* Default implementation of {@link WebTestClient}.
*
@ -384,7 +383,22 @@ class DefaultWebTestClient implements WebTestClient {
@Override
public <T extends S> T isEqualTo(B expected) {
this.result.assertWithDiagnostics(() ->
assertEquals("Response body", expected, this.result.getResponseBody()));
AssertionErrors.assertEquals("Response body", expected, this.result.getResponseBody()));
return self();
}
@Override
public <T extends S> T value(Matcher<B> matcher) {
this.result.assertWithDiagnostics(() -> MatcherAssert.assertThat(this.result.getResponseBody(), matcher));
return self();
}
@Override
public <T extends S, R> T value(Function<B, R> bodyMapper, Matcher<R> matcher) {
this.result.assertWithDiagnostics(() -> {
B body = this.result.getResponseBody();
MatcherAssert.assertThat(bodyMapper.apply(body), matcher);
});
return self();
}
@ -417,7 +431,8 @@ class DefaultWebTestClient implements WebTestClient {
public ListBodySpec<E> hasSize(int size) {
List<E> actual = getResult().getResponseBody();
String message = "Response body does not contain " + size + " elements";
getResult().assertWithDiagnostics(() -> assertEquals(message, size, (actual != null ? actual.size() : 0)));
getResult().assertWithDiagnostics(() ->
AssertionErrors.assertEquals(message, size, (actual != null ? actual.size() : 0)));
return this;
}
@ -427,7 +442,8 @@ class DefaultWebTestClient implements WebTestClient {
List<E> expected = Arrays.asList(elements);
List<E> actual = getResult().getResponseBody();
String message = "Response body does not contain " + expected;
getResult().assertWithDiagnostics(() -> assertTrue(message, (actual != null && actual.containsAll(expected))));
getResult().assertWithDiagnostics(() ->
AssertionErrors.assertTrue(message, (actual != null && actual.containsAll(expected))));
return this;
}
@ -437,7 +453,8 @@ class DefaultWebTestClient implements WebTestClient {
List<E> expected = Arrays.asList(elements);
List<E> actual = getResult().getResponseBody();
String message = "Response body should not have contained " + expected;
getResult().assertWithDiagnostics(() -> assertTrue(message, (actual == null || !actual.containsAll(expected))));
getResult().assertWithDiagnostics(() ->
AssertionErrors.assertTrue(message, (actual == null || !actual.containsAll(expected))));
return this;
}
@ -461,7 +478,8 @@ class DefaultWebTestClient implements WebTestClient {
@Override
public EntityExchangeResult<Void> isEmpty() {
this.result.assertWithDiagnostics(() -> assertTrue("Expected empty body", this.isEmpty));
this.result.assertWithDiagnostics(() ->
AssertionErrors.assertTrue("Expected empty body", this.isEmpty));
return new EntityExchangeResult<>(this.result, null);
}
@ -506,8 +524,8 @@ class DefaultWebTestClient implements WebTestClient {
if (body == null || body.length == 0) {
return "";
}
MediaType mediaType = this.result.getResponseHeaders().getContentType();
Charset charset = Optional.ofNullable(mediaType).map(MimeType::getCharset).orElse(UTF_8);
Charset charset = Optional.ofNullable(this.result.getResponseHeaders().getContentType())
.map(MimeType::getCharset).orElse(StandardCharsets.UTF_8);
return new String(body, charset);
}

View File

@ -17,17 +17,16 @@
package org.springframework.test.web.reactive.server;
import java.util.Arrays;
import java.util.regex.Pattern;
import org.hamcrest.Matcher;
import org.hamcrest.MatcherAssert;
import org.springframework.http.CacheControl;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import static org.springframework.test.util.AssertionErrors.assertEquals;
import static org.springframework.test.util.AssertionErrors.assertTrue;
import static org.springframework.test.util.AssertionErrors.fail;
import org.springframework.test.util.AssertionErrors;
/**
* Assertions on headers of the response.
@ -59,20 +58,35 @@ public class HeaderAssertions {
}
/**
* Expect a header with the given name whose first value matches the
* provided regex pattern.
* Match the primary value of the response header with a regex.
* @param name the header name
* @param pattern the String pattern to pass to {@link Pattern#compile(String)}
* @param pattern the regex pattern
*/
public WebTestClient.ResponseSpec valueMatches(String name, String pattern) {
String value = getRequiredValue(name);
String message = getMessage(name) + "=[" + value + "] does not match [" + pattern + "]";
this.exchangeResult.assertWithDiagnostics(() -> AssertionErrors.assertTrue(message, value.matches(pattern)));
return this.responseSpec;
}
/**
* Assert the primary value of the response header with a {@link Matcher}.
* @param name the header name
* @param matcher the matcher to sue
* @since 5.1
*/
public WebTestClient.ResponseSpec value(String name, Matcher<? super String> matcher) {
String value = getRequiredValue(name);
this.exchangeResult.assertWithDiagnostics(() -> MatcherAssert.assertThat(value, matcher));
return this.responseSpec;
}
private String getRequiredValue(String name) {
String value = getHeaders().getFirst(name);
if (value == null) {
fail(getMessage(name) + " not found");
AssertionErrors.fail(getMessage(name) + " not found");
}
boolean match = Pattern.compile(pattern).matcher(value).matches();
String message = getMessage(name) + "=[" + value + "] does not match [" + pattern + "]";
this.exchangeResult.assertWithDiagnostics(() -> assertTrue(message, match));
return this.responseSpec;
return value;
}
/**
@ -82,7 +96,7 @@ public class HeaderAssertions {
public WebTestClient.ResponseSpec exists(String name) {
if (!getHeaders().containsKey(name)) {
String message = getMessage(name) + " does not exist";
this.exchangeResult.assertWithDiagnostics(() -> fail(message));
this.exchangeResult.assertWithDiagnostics(() -> AssertionErrors.fail(message));
}
return this.responseSpec;
}
@ -93,7 +107,7 @@ public class HeaderAssertions {
public WebTestClient.ResponseSpec doesNotExist(String name) {
if (getHeaders().containsKey(name)) {
String message = getMessage(name) + " exists with value=[" + getHeaders().getFirst(name) + "]";
this.exchangeResult.assertWithDiagnostics(() -> fail(message));
this.exchangeResult.assertWithDiagnostics(() -> AssertionErrors.fail(message));
}
return this.responseSpec;
}
@ -140,7 +154,7 @@ public class HeaderAssertions {
MediaType actual = getHeaders().getContentType();
String message = getMessage("Content-Type") + "=[" + actual + "] is not compatible with [" + mediaType + "]";
this.exchangeResult.assertWithDiagnostics(() ->
assertTrue(message, (actual != null && actual.isCompatibleWith(mediaType))));
AssertionErrors.assertTrue(message, (actual != null && actual.isCompatibleWith(mediaType))));
return this.responseSpec;
}
@ -175,7 +189,10 @@ public class HeaderAssertions {
}
private WebTestClient.ResponseSpec assertHeader(String name, @Nullable Object expected, @Nullable Object actual) {
this.exchangeResult.assertWithDiagnostics(() -> assertEquals(getMessage(name), expected, actual));
this.exchangeResult.assertWithDiagnostics(() -> {
String message = getMessage(name);
AssertionErrors.assertEquals(message, expected, actual);
});
return this.responseSpec;
}

View File

@ -16,6 +16,8 @@
package org.springframework.test.web.reactive.server;
import org.hamcrest.Matcher;
import org.springframework.test.util.JsonPathExpectationsHelper;
/**
@ -132,4 +134,22 @@ public class JsonPathAssertions {
return this.bodySpec;
}
/**
* Delegates to {@link JsonPathExpectationsHelper#assertValue(String, Matcher)}.
* @since 5.1
*/
public <T> WebTestClient.BodyContentSpec value(Matcher<T> matcher) {
this.pathHelper.assertValue(this.content, matcher);
return this.bodySpec;
}
/**
* Delegates to {@link JsonPathExpectationsHelper#assertValue(String, Matcher, Class)}.
* @since 5.1
*/
public <T> WebTestClient.BodyContentSpec value(Matcher<T> matcher, Class<T> targetType) {
this.pathHelper.assertValue(this.content, matcher, targetType);
return this.bodySpec;
}
}

View File

@ -16,9 +16,11 @@
package org.springframework.test.web.reactive.server;
import org.springframework.http.HttpStatus;
import org.hamcrest.Matcher;
import org.hamcrest.MatcherAssert;
import static org.springframework.test.util.AssertionErrors.assertEquals;
import org.springframework.http.HttpStatus;
import org.springframework.test.util.AssertionErrors;
/**
* Assertions on the response status.
@ -52,7 +54,7 @@ public class StatusAssertions {
*/
public WebTestClient.ResponseSpec isEqualTo(int status) {
int actual = this.exchangeResult.getStatus().value();
this.exchangeResult.assertWithDiagnostics(() -> assertEquals("Status", status, actual));
this.exchangeResult.assertWithDiagnostics(() -> AssertionErrors.assertEquals("Status", status, actual));
return this.responseSpec;
}
@ -155,7 +157,7 @@ public class StatusAssertions {
public WebTestClient.ResponseSpec reasonEquals(String reason) {
String actual = this.exchangeResult.getStatus().getReasonPhrase();
String message = "Response status reason";
this.exchangeResult.assertWithDiagnostics(() -> assertEquals(message, reason, actual));
this.exchangeResult.assertWithDiagnostics(() -> AssertionErrors.assertEquals(message, reason, actual));
return this.responseSpec;
}
@ -195,17 +197,30 @@ public class StatusAssertions {
return assertSeriesAndReturn(expected);
}
/**
* Match the response status value with a Hamcrest matcher.
* @param matcher the matcher to use
* @since 5.1
*/
public WebTestClient.ResponseSpec value(Matcher<Integer> matcher) {
int value = this.exchangeResult.getStatus().value();
this.exchangeResult.assertWithDiagnostics(() -> MatcherAssert.assertThat("Response status", value, matcher));
return this.responseSpec;
}
private WebTestClient.ResponseSpec assertStatusAndReturn(HttpStatus expected) {
HttpStatus actual = this.exchangeResult.getStatus();
this.exchangeResult.assertWithDiagnostics(() -> assertEquals("Status", expected, actual));
this.exchangeResult.assertWithDiagnostics(() -> AssertionErrors.assertEquals("Status", expected, actual));
return this.responseSpec;
}
private WebTestClient.ResponseSpec assertSeriesAndReturn(HttpStatus.Series expected) {
HttpStatus status = this.exchangeResult.getStatus();
this.exchangeResult.assertWithDiagnostics(() ->
assertEquals("Range for response status value " + status, expected, status.series()));
this.exchangeResult.assertWithDiagnostics(() -> {
String message = "Range for response status value " + status;
AssertionErrors.assertEquals(message, expected, status.series());
});
return this.responseSpec;
}

View File

@ -25,6 +25,7 @@ import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import org.hamcrest.Matcher;
import org.reactivestreams.Publisher;
import org.springframework.context.ApplicationContext;
@ -758,6 +759,19 @@ public interface WebTestClient {
*/
<T extends S> T isEqualTo(B expected);
/**
* Assert the extracted body with a {@link Matcher}.
* @since 5.1
*/
<T extends S> T value(Matcher<B> matcher);
/**
* Transform the extracted the body with a function, e.g. extracting a
* property, and assert the mapped value with a {@link Matcher}.
* @since 5.1
*/
<T extends S, R> T value(Function<B, R> bodyMapper, Matcher<R> matcher);
/**
* Assert the exchange result with the given {@link Consumer}.
*/
@ -851,8 +865,8 @@ public interface WebTestClient {
* 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
* @see #xpath(String, Map, Object...)
*/
default XpathAssertions xpath(String expression, Object... args){
return xpath(expression, null, args);

View File

@ -21,6 +21,8 @@ import java.util.Map;
import java.util.Optional;
import javax.xml.xpath.XPathExpressionException;
import org.hamcrest.Matcher;
import org.springframework.http.HttpHeaders;
import org.springframework.lang.Nullable;
import org.springframework.test.util.XpathExpectationsHelper;
@ -62,47 +64,68 @@ public class XpathAssertions {
/**
* Delegates to {@link XpathExpectationsHelper#assertString(byte[], String, String)}
* Delegates to {@link XpathExpectationsHelper#assertString(byte[], String, String)}.
*/
public WebTestClient.BodyContentSpec isEqualTo(String expectedValue) {
return assertWith(() -> this.xpathHelper.assertString(getContent(), getCharset(), expectedValue));
}
/**
* Delegates to {@link XpathExpectationsHelper#assertNumber(byte[], String, Double)}
* Delegates to {@link XpathExpectationsHelper#assertNumber(byte[], String, Double)}.
*/
public WebTestClient.BodyContentSpec isEqualTo(Double expectedValue) {
return assertWith(() -> this.xpathHelper.assertNumber(getContent(), getCharset(), expectedValue));
}
/**
* Delegates to {@link XpathExpectationsHelper#assertBoolean(byte[], String, boolean)}
* Delegates to {@link XpathExpectationsHelper#assertBoolean(byte[], String, boolean)}.
*/
public WebTestClient.BodyContentSpec isEqualTo(boolean expectedValue) {
return assertWith(() -> this.xpathHelper.assertBoolean(getContent(), getCharset(), expectedValue));
}
/**
* Delegates to {@link XpathExpectationsHelper#exists(byte[], String)}
* Delegates to {@link XpathExpectationsHelper#exists(byte[], String)}.
*/
public WebTestClient.BodyContentSpec exists() {
return assertWith(() -> this.xpathHelper.exists(getContent(), getCharset()));
}
/**
* Delegates to {@link XpathExpectationsHelper#doesNotExist(byte[], String)}
* Delegates to {@link XpathExpectationsHelper#doesNotExist(byte[], String)}.
*/
public WebTestClient.BodyContentSpec doesNotExist() {
return assertWith(() -> this.xpathHelper.doesNotExist(getContent(), getCharset()));
}
/**
* Delegates to {@link XpathExpectationsHelper[#assertNodeCount(byte[], String, int)}
* Delegates to {@link XpathExpectationsHelper#assertNodeCount(byte[], String, int)}.
*/
public WebTestClient.BodyContentSpec nodeCount(int expectedCount) {
return assertWith(() -> this.xpathHelper.assertNodeCount(getContent(), getCharset(), expectedCount));
}
/**
* Delegates to {@link XpathExpectationsHelper#assertString(byte[], String, Matcher)}.
*/
public WebTestClient.BodyContentSpec string(Matcher<? super String> matcher){
return assertWith(() -> this.xpathHelper.assertString(getContent(), getCharset(), matcher));
}
/**
* Delegates to {@link XpathExpectationsHelper#assertNumber(byte[], String, Matcher)}.
*/
public WebTestClient.BodyContentSpec number(Matcher<? super Double> matcher){
return assertWith(() -> this.xpathHelper.assertNumber(getContent(), getCharset(), matcher));
}
/**
* Delegates to {@link XpathExpectationsHelper#assertNodeCount(byte[], String, Matcher)}.
*/
public WebTestClient.BodyContentSpec nodeCount(Matcher<Integer> matcher){
return assertWith(() -> this.xpathHelper.assertNodeCount(getContent(), getCharset(), matcher));
}
private WebTestClient.BodyContentSpec assertWith(CheckedExceptionTask task) {
try {
@ -139,4 +162,4 @@ public class XpathAssertions {
void run() throws Exception;
}
}
}

View File

@ -30,6 +30,7 @@ import org.springframework.http.MediaType;
import org.springframework.mock.http.client.reactive.MockClientHttpRequest;
import org.springframework.mock.http.client.reactive.MockClientHttpResponse;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
@ -125,6 +126,15 @@ public class HeaderAssertionTests {
}
}
@Test
public void valueMatcher() {
HttpHeaders headers = new HttpHeaders();
headers.add("foo", "bar");
HeaderAssertions assertions = headerAssertions(headers);
assertions.value("foo", containsString("a"));
}
@Test
public void exists() {
HttpHeaders headers = new HttpHeaders();

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* 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.
@ -26,6 +26,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.mock.http.client.reactive.MockClientHttpRequest;
import org.springframework.mock.http.client.reactive.MockClientHttpResponse;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
@ -158,6 +159,23 @@ public class StatusAssertionTests {
}
}
@Test
public void matches() {
StatusAssertions assertions = statusAssertions(HttpStatus.CONFLICT);
// Success
assertions.value(equalTo(409));
assertions.value(greaterThan(400));
try {
assertions.value(equalTo(200));
fail("Wrong status expected");
}
catch (AssertionError error) {
// Expected
}
}
private StatusAssertions statusAssertions(HttpStatus status) {
MockClientHttpRequest request = new MockClientHttpRequest(HttpMethod.GET, URI.create("/"));

View File

@ -31,6 +31,8 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import static org.hamcrest.Matchers.*;
/**
* Samples of tests using {@link WebTestClient} with serialized JSON content.
*
@ -64,16 +66,14 @@ public class JsonContentTests {
.jsonPath("$[2].name").isEqualTo("John");
}
@Test // https://stackoverflow.com/questions/49149376/webtestclient-check-that-jsonpath-contains-sub-string
public void jsonPathContainsSubstringViaRegex() {
@Test
public void jsonPathMatches() {
this.client.get().uri("/persons/John")
.accept(MediaType.APPLICATION_JSON_UTF8)
.exchange()
.expectStatus().isOk()
.expectBody()
// The following determines if at least one person is returned with a
// name containing "oh", and "John" matches that.
.jsonPath("$[?(@.name =~ /.*oh.*/)].name").hasJsonPath();
.jsonPath("$.name").value(containsString("oh"));
}
@Test

View File

@ -38,11 +38,10 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import static java.time.Duration.ofMillis;
import static org.hamcrest.CoreMatchers.endsWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.springframework.http.MediaType.TEXT_EVENT_STREAM;
import static java.time.Duration.*;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.springframework.http.MediaType.*;
/**
* Annotated controllers accepting and returning typed Objects.
@ -67,6 +66,15 @@ public class ResponseEntityTests {
.expectBody(Person.class).isEqualTo(new Person("John"));
}
@Test
public void entityMatcher() {
this.client.get().uri("/John")
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
.expectBody(Person.class).value(Person::getName, startsWith("Joh"));
}
@Test
public void entityWithConsumer() {
this.client.get().uri("/John")

View File

@ -38,6 +38,8 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import static org.hamcrest.Matchers.*;
/**
* Samples of tests using {@link WebTestClient} with XML content.
*
@ -83,6 +85,16 @@ public class XmlContentTests {
.xpath("/persons/person[3]/name").isEqualTo("John");
}
@Test
public void xpathMatches() {
this.client.get().uri("/persons")
.accept(MediaType.APPLICATION_XML)
.exchange()
.expectStatus().isOk()
.expectBody()
.xpath("//person/name").string(startsWith("J"));
}
@Test
public void xpathContainsSubstringViaRegex() {
this.client.get().uri("/persons/John")