Allow MockRest to match header/queryParam value list with one Matcher
This commit adds a `header` variant and a `queryParam` variant to the `MockRestRequestMatchers` API which take a single `Matcher` over the list of values. Contrary to the vararg variants, the whole list is evaluated and the caller can choose the desired semantics using readily-available iterable matchers like `everyItem`, `hasItems`, `hasSize`, `contains` or `containsInAnyOrder`... The fact that the previous variants don't strictly check the size of the actual list == the number of provided matchers or expected values is now documented in their respective javadocs. See gh-28660 Closes gh-29953
This commit is contained in:
parent
79a1fcb099
commit
189d4e3e4c
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2020 the original author or authors.
|
* Copyright 2002-2023 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -23,6 +23,7 @@ import java.util.Map;
|
||||||
import javax.xml.xpath.XPathExpressionException;
|
import javax.xml.xpath.XPathExpressionException;
|
||||||
|
|
||||||
import org.hamcrest.Matcher;
|
import org.hamcrest.Matcher;
|
||||||
|
import org.hamcrest.Matchers;
|
||||||
|
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.http.client.ClientHttpRequest;
|
import org.springframework.http.client.ClientHttpRequest;
|
||||||
|
|
@ -114,6 +115,11 @@ public abstract class MockRestRequestMatchers {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assert request query parameter values with the given Hamcrest matcher(s).
|
* Assert request query parameter values with the given Hamcrest matcher(s).
|
||||||
|
* <p> Note that if the queryParam value list is larger than the number of provided
|
||||||
|
* {@code matchers}, extra values are considered acceptable.
|
||||||
|
* See {@link #queryParam(String, Matcher)} for a variant that takes a
|
||||||
|
* {@code Matcher} over the whole list of values.
|
||||||
|
* @see #queryParam(String, Matcher)
|
||||||
*/
|
*/
|
||||||
@SafeVarargs
|
@SafeVarargs
|
||||||
public static RequestMatcher queryParam(String name, Matcher<? super String>... matchers) {
|
public static RequestMatcher queryParam(String name, Matcher<? super String>... matchers) {
|
||||||
|
|
@ -128,6 +134,11 @@ public abstract class MockRestRequestMatchers {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assert request query parameter values.
|
* Assert request query parameter values.
|
||||||
|
* <p> Note that if the queryParam value list is larger than {@code expectedValues},
|
||||||
|
* extra values are considered acceptable.
|
||||||
|
* See {@link #queryParam(String, Matcher)} for a variant that takes a
|
||||||
|
* {@code Matcher} over the whole list of values.
|
||||||
|
* @see #queryParam(String, Matcher)
|
||||||
*/
|
*/
|
||||||
public static RequestMatcher queryParam(String name, String... expectedValues) {
|
public static RequestMatcher queryParam(String name, String... expectedValues) {
|
||||||
return request -> {
|
return request -> {
|
||||||
|
|
@ -139,6 +150,29 @@ public abstract class MockRestRequestMatchers {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert request query parameter, matching on the whole {@code List} of values.
|
||||||
|
* <p> This can be used to check that the list has at least one value matching a
|
||||||
|
* criteria ({@link Matchers#hasItem(Matcher)}), or that every value in the list
|
||||||
|
* matches a common criteria ({@link Matchers#everyItem(Matcher)}), or that each
|
||||||
|
* value in the list matches its corresponding dedicated criteria
|
||||||
|
* ({@link Matchers#contains(Matcher[])}, and more.
|
||||||
|
* @param name the name of the queryParam to consider
|
||||||
|
* @param matcher the matcher to apply to the whole list of values for that header
|
||||||
|
* @since 6.0.5
|
||||||
|
*/
|
||||||
|
public static RequestMatcher queryParam(String name, Matcher<? super List<String>> matcher) {
|
||||||
|
return request -> {
|
||||||
|
MultiValueMap<String, String> params = getQueryParams(request);
|
||||||
|
List<String> paramValues = params.get(name);
|
||||||
|
if (paramValues == null) {
|
||||||
|
fail("No queryParam [" + name + "]");
|
||||||
|
}
|
||||||
|
assertThat("Request queryParam values for [" + name + "]", paramValues, matcher);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private static MultiValueMap<String, String> getQueryParams(ClientHttpRequest request) {
|
private static MultiValueMap<String, String> getQueryParams(ClientHttpRequest request) {
|
||||||
return UriComponentsBuilder.fromUri(request.getURI()).build().getQueryParams();
|
return UriComponentsBuilder.fromUri(request.getURI()).build().getQueryParams();
|
||||||
}
|
}
|
||||||
|
|
@ -158,6 +192,11 @@ public abstract class MockRestRequestMatchers {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assert request header values with the given Hamcrest matcher(s).
|
* Assert request header values with the given Hamcrest matcher(s).
|
||||||
|
* <p> Note that if the header's value list is larger than the number of provided
|
||||||
|
* {@code matchers}, extra values are considered acceptable.
|
||||||
|
* See {@link #header(String, Matcher)} for a variant that takes a {@code Matcher}
|
||||||
|
* over the whole list of values.
|
||||||
|
* @see #header(String, Matcher)
|
||||||
*/
|
*/
|
||||||
@SafeVarargs
|
@SafeVarargs
|
||||||
public static RequestMatcher header(String name, Matcher<? super String>... matchers) {
|
public static RequestMatcher header(String name, Matcher<? super String>... matchers) {
|
||||||
|
|
@ -173,6 +212,11 @@ public abstract class MockRestRequestMatchers {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assert request header values.
|
* Assert request header values.
|
||||||
|
* <p> Note that if the header's value list is larger than {@code expectedValues},
|
||||||
|
* extra values are considered acceptable.
|
||||||
|
* See {@link #header(String, Matcher)} for a variant that takes a {@code Matcher}
|
||||||
|
* over the whole list of values.
|
||||||
|
* @see #header(String, Matcher)
|
||||||
*/
|
*/
|
||||||
public static RequestMatcher header(String name, String... expectedValues) {
|
public static RequestMatcher header(String name, String... expectedValues) {
|
||||||
return request -> {
|
return request -> {
|
||||||
|
|
@ -185,6 +229,27 @@ public abstract class MockRestRequestMatchers {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert request header, matching on the whole {@code List} of values.
|
||||||
|
* <p> This can be used to check that the list has at least one value matching a
|
||||||
|
* criteria ({@link Matchers#hasItem(Matcher)}), or that every value in the list
|
||||||
|
* matches a common criteria ({@link Matchers#everyItem(Matcher)}), or that each
|
||||||
|
* value in the list matches its corresponding dedicated criteria
|
||||||
|
* ({@link Matchers#contains(Matcher[])}, and more.
|
||||||
|
* @param name the name of the header to consider
|
||||||
|
* @param matcher the matcher to apply to the whole list of values for that header
|
||||||
|
* @since 6.0.5
|
||||||
|
*/
|
||||||
|
public static RequestMatcher header(String name, Matcher<? super List<String>> matcher) {
|
||||||
|
return request -> {
|
||||||
|
List<String> headerValues = request.getHeaders().get(name);
|
||||||
|
if (headerValues == null) {
|
||||||
|
fail("No header values for header [" + name + "]");
|
||||||
|
}
|
||||||
|
assertThat("Request header values for [" + name + "]", headerValues, matcher);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assert that the given request header does not exist.
|
* Assert that the given request header does not exist.
|
||||||
* @since 5.2
|
* @since 5.2
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2022 the original author or authors.
|
* Copyright 2002-2023 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -16,18 +16,35 @@
|
||||||
|
|
||||||
package org.springframework.test.web.client.match;
|
package org.springframework.test.web.client.match;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.hamcrest.CoreMatchers;
|
||||||
|
import org.hamcrest.Matchers;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.mock.http.client.MockClientHttpRequest;
|
import org.springframework.mock.http.client.MockClientHttpRequest;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.hamcrest.Matchers.allOf;
|
||||||
|
import static org.hamcrest.Matchers.any;
|
||||||
|
import static org.hamcrest.Matchers.anything;
|
||||||
|
import static org.hamcrest.Matchers.contains;
|
||||||
|
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||||
import static org.hamcrest.Matchers.containsString;
|
import static org.hamcrest.Matchers.containsString;
|
||||||
|
import static org.hamcrest.Matchers.endsWith;
|
||||||
|
import static org.hamcrest.Matchers.everyItem;
|
||||||
|
import static org.hamcrest.Matchers.hasItem;
|
||||||
|
import static org.hamcrest.Matchers.hasSize;
|
||||||
|
import static org.hamcrest.Matchers.is;
|
||||||
|
import static org.hamcrest.Matchers.notNullValue;
|
||||||
|
import static org.hamcrest.Matchers.nullValue;
|
||||||
|
import static org.hamcrest.Matchers.startsWith;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit tests for {@link MockRestRequestMatchers}.
|
* Unit tests for {@link MockRestRequestMatchers}.
|
||||||
|
|
@ -146,6 +163,63 @@ class MockRestRequestMatchersTests {
|
||||||
.hasMessageContaining("was \"bar\"");
|
.hasMessageContaining("was \"bar\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void headerListMissing() {
|
||||||
|
assertThatThrownBy(() -> MockRestRequestMatchers.header("foo", hasSize(2)).match(this.request))
|
||||||
|
.isInstanceOf(AssertionError.class)
|
||||||
|
.hasMessage("No header values for header [foo]");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void headerListMatchers() throws IOException {
|
||||||
|
this.request.getHeaders().put("foo", Arrays.asList("bar", "baz"));
|
||||||
|
|
||||||
|
MockRestRequestMatchers.header("foo", containsInAnyOrder(endsWith("baz"), endsWith("bar"))).match(this.request);
|
||||||
|
MockRestRequestMatchers.header("foo", contains(is("bar"), is("baz"))).match(this.request);
|
||||||
|
MockRestRequestMatchers.header("foo", contains(is("bar"), Matchers.anything())).match(this.request);
|
||||||
|
MockRestRequestMatchers.header("foo", hasItem(endsWith("baz"))).match(this.request);
|
||||||
|
MockRestRequestMatchers.header("foo", everyItem(startsWith("ba"))).match(this.request);
|
||||||
|
MockRestRequestMatchers.header("foo", hasSize(2)).match(this.request);
|
||||||
|
|
||||||
|
//these can be a bit ambiguous when reading the test (the compiler selects the list matcher):
|
||||||
|
MockRestRequestMatchers.header("foo", notNullValue()).match(this.request);
|
||||||
|
MockRestRequestMatchers.header("foo", is(anything())).match(this.request);
|
||||||
|
MockRestRequestMatchers.header("foo", allOf(notNullValue(), notNullValue())).match(this.request);
|
||||||
|
|
||||||
|
//these are not as ambiguous thanks to an inner matcher that is either obviously list-oriented,
|
||||||
|
//string-oriented or obviously a vararg of matchers
|
||||||
|
//list matcher version
|
||||||
|
MockRestRequestMatchers.header("foo", allOf(notNullValue(), hasSize(2))).match(this.request);
|
||||||
|
//vararg version
|
||||||
|
MockRestRequestMatchers.header("foo", allOf(notNullValue(), endsWith("ar"))).match(this.request);
|
||||||
|
MockRestRequestMatchers.header("foo", is((any(String.class)))).match(this.request);
|
||||||
|
MockRestRequestMatchers.header("foo", CoreMatchers.either(is("bar")).or(is(nullValue()))).match(this.request);
|
||||||
|
MockRestRequestMatchers.header("foo", is(notNullValue()), is(notNullValue())).match(this.request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void headerListContainsMismatch() {
|
||||||
|
this.request.getHeaders().put("foo", Arrays.asList("bar", "baz"));
|
||||||
|
|
||||||
|
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> MockRestRequestMatchers
|
||||||
|
.header("foo", contains(containsString("ba"))).match(this.request))
|
||||||
|
.withMessage("Request header values for [foo]\n"
|
||||||
|
+ "Expected: iterable containing [a string containing \"ba\"]\n"
|
||||||
|
+ " but: not matched: \"baz\"");
|
||||||
|
|
||||||
|
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> MockRestRequestMatchers
|
||||||
|
.header("foo", hasItem(endsWith("ba"))).match(this.request))
|
||||||
|
.withMessage("Request header values for [foo]\n"
|
||||||
|
+ "Expected: a collection containing a string ending with \"ba\"\n"
|
||||||
|
+ " but: mismatches were: [was \"bar\", was \"baz\"]");
|
||||||
|
|
||||||
|
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> MockRestRequestMatchers
|
||||||
|
.header("foo", everyItem(endsWith("ar"))).match(this.request))
|
||||||
|
.withMessage("Request header values for [foo]\n"
|
||||||
|
+ "Expected: every item is a string ending with \"ar\"\n"
|
||||||
|
+ " but: an item was \"baz\"");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void headers() throws Exception {
|
void headers() throws Exception {
|
||||||
this.request.getHeaders().put("foo", Arrays.asList("bar", "baz"));
|
this.request.getHeaders().put("foo", Arrays.asList("bar", "baz"));
|
||||||
|
|
@ -210,4 +284,62 @@ class MockRestRequestMatchersTests {
|
||||||
.hasMessageContaining("was \"bar\"");
|
.hasMessageContaining("was \"bar\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void queryParamListMissing() {
|
||||||
|
assertThatThrownBy(() -> MockRestRequestMatchers.queryParam("foo", hasSize(2)).match(this.request))
|
||||||
|
.isInstanceOf(AssertionError.class)
|
||||||
|
.hasMessage("No queryParam [foo]");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void queryParamListMatchers() throws IOException {
|
||||||
|
this.request.setURI(URI.create("http://www.foo.example/a?foo=bar&foo=baz"));
|
||||||
|
|
||||||
|
MockRestRequestMatchers.queryParam("foo", containsInAnyOrder(endsWith("baz"), endsWith("bar"))).match(this.request);
|
||||||
|
MockRestRequestMatchers.queryParam("foo", contains(is("bar"), is("baz"))).match(this.request);
|
||||||
|
MockRestRequestMatchers.queryParam("foo", contains(is("bar"), Matchers.anything())).match(this.request);
|
||||||
|
MockRestRequestMatchers.queryParam("foo", hasItem(endsWith("baz"))).match(this.request);
|
||||||
|
MockRestRequestMatchers.queryParam("foo", everyItem(startsWith("ba"))).match(this.request);
|
||||||
|
MockRestRequestMatchers.queryParam("foo", hasSize(2)).match(this.request);
|
||||||
|
|
||||||
|
//these can be a bit ambiguous when reading the test (the compiler selects the list matcher):
|
||||||
|
MockRestRequestMatchers.queryParam("foo", notNullValue()).match(this.request);
|
||||||
|
MockRestRequestMatchers.queryParam("foo", is(anything())).match(this.request);
|
||||||
|
MockRestRequestMatchers.queryParam("foo", allOf(notNullValue(), notNullValue())).match(this.request);
|
||||||
|
|
||||||
|
//these are not as ambiguous thanks to an inner matcher that is either obviously list-oriented,
|
||||||
|
//string-oriented or obviously a vararg of matchers
|
||||||
|
//list matcher version
|
||||||
|
MockRestRequestMatchers.queryParam("foo", allOf(notNullValue(), hasSize(2))).match(this.request);
|
||||||
|
//vararg version
|
||||||
|
MockRestRequestMatchers.queryParam("foo", allOf(notNullValue(), endsWith("ar"))).match(this.request);
|
||||||
|
MockRestRequestMatchers.queryParam("foo", is((any(String.class)))).match(this.request);
|
||||||
|
MockRestRequestMatchers.queryParam("foo", CoreMatchers.either(is("bar")).or(is(nullValue()))).match(this.request);
|
||||||
|
MockRestRequestMatchers.queryParam("foo", is(notNullValue()), is(notNullValue())).match(this.request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void queryParamListContainsMismatch() {
|
||||||
|
this.request.setURI(URI.create("http://www.foo.example/a?foo=bar&foo=baz"));
|
||||||
|
|
||||||
|
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> MockRestRequestMatchers
|
||||||
|
.queryParam("foo", contains(containsString("ba"))).match(this.request))
|
||||||
|
.withMessage("Request queryParam values for [foo]\n"
|
||||||
|
+ "Expected: iterable containing [a string containing \"ba\"]\n"
|
||||||
|
+ " but: not matched: \"baz\"");
|
||||||
|
|
||||||
|
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> MockRestRequestMatchers
|
||||||
|
.queryParam("foo", hasItem(endsWith("ba"))).match(this.request))
|
||||||
|
.withMessage("Request queryParam values for [foo]\n"
|
||||||
|
+ "Expected: a collection containing a string ending with \"ba\"\n"
|
||||||
|
+ " but: mismatches were: [was \"bar\", was \"baz\"]");
|
||||||
|
|
||||||
|
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> MockRestRequestMatchers
|
||||||
|
.queryParam("foo", everyItem(endsWith("ar"))).match(this.request))
|
||||||
|
.withMessage("Request queryParam values for [foo]\n"
|
||||||
|
+ "Expected: every item is a string ending with \"ar\"\n"
|
||||||
|
+ " but: an item was \"baz\"");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue