Skip to content

Commit 189d4e3

Browse files
authored
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
1 parent 79a1fcb commit 189d4e3

File tree

2 files changed

+199
-2
lines changed

2 files changed

+199
-2
lines changed

spring-test/src/main/java/org/springframework/test/web/client/match/MockRestRequestMatchers.java

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@
2323
import javax.xml.xpath.XPathExpressionException;
2424

2525
import org.hamcrest.Matcher;
26+
import org.hamcrest.Matchers;
2627

2728
import org.springframework.http.HttpMethod;
2829
import org.springframework.http.client.ClientHttpRequest;
@@ -114,6 +115,11 @@ public static RequestMatcher requestTo(URI uri) {
114115

115116
/**
116117
* Assert request query parameter values with the given Hamcrest matcher(s).
118+
* <p> Note that if the queryParam value list is larger than the number of provided
119+
* {@code matchers}, extra values are considered acceptable.
120+
* See {@link #queryParam(String, Matcher)} for a variant that takes a
121+
* {@code Matcher} over the whole list of values.
122+
* @see #queryParam(String, Matcher)
117123
*/
118124
@SafeVarargs
119125
public static RequestMatcher queryParam(String name, Matcher<? super String>... matchers) {
@@ -128,6 +134,11 @@ public static RequestMatcher queryParam(String name, Matcher<? super String>...
128134

129135
/**
130136
* Assert request query parameter values.
137+
* <p> Note that if the queryParam value list is larger than {@code expectedValues},
138+
* extra values are considered acceptable.
139+
* See {@link #queryParam(String, Matcher)} for a variant that takes a
140+
* {@code Matcher} over the whole list of values.
141+
* @see #queryParam(String, Matcher)
131142
*/
132143
public static RequestMatcher queryParam(String name, String... expectedValues) {
133144
return request -> {
@@ -139,6 +150,29 @@ public static RequestMatcher queryParam(String name, String... expectedValues) {
139150
};
140151
}
141152

153+
/**
154+
* Assert request query parameter, matching on the whole {@code List} of values.
155+
* <p> This can be used to check that the list has at least one value matching a
156+
* criteria ({@link Matchers#hasItem(Matcher)}), or that every value in the list
157+
* matches a common criteria ({@link Matchers#everyItem(Matcher)}), or that each
158+
* value in the list matches its corresponding dedicated criteria
159+
* ({@link Matchers#contains(Matcher[])}, and more.
160+
* @param name the name of the queryParam to consider
161+
* @param matcher the matcher to apply to the whole list of values for that header
162+
* @since 6.0.5
163+
*/
164+
public static RequestMatcher queryParam(String name, Matcher<? super List<String>> matcher) {
165+
return request -> {
166+
MultiValueMap<String, String> params = getQueryParams(request);
167+
List<String> paramValues = params.get(name);
168+
if (paramValues == null) {
169+
fail("No queryParam [" + name + "]");
170+
}
171+
assertThat("Request queryParam values for [" + name + "]", paramValues, matcher);
172+
};
173+
}
174+
175+
142176
private static MultiValueMap<String, String> getQueryParams(ClientHttpRequest request) {
143177
return UriComponentsBuilder.fromUri(request.getURI()).build().getQueryParams();
144178
}
@@ -158,6 +192,11 @@ private static void assertValueCount(
158192

159193
/**
160194
* Assert request header values with the given Hamcrest matcher(s).
195+
* <p> Note that if the header's value list is larger than the number of provided
196+
* {@code matchers}, extra values are considered acceptable.
197+
* See {@link #header(String, Matcher)} for a variant that takes a {@code Matcher}
198+
* over the whole list of values.
199+
* @see #header(String, Matcher)
161200
*/
162201
@SafeVarargs
163202
public static RequestMatcher header(String name, Matcher<? super String>... matchers) {
@@ -173,6 +212,11 @@ public static RequestMatcher header(String name, Matcher<? super String>... matc
173212

174213
/**
175214
* Assert request header values.
215+
* <p> Note that if the header's value list is larger than {@code expectedValues},
216+
* extra values are considered acceptable.
217+
* See {@link #header(String, Matcher)} for a variant that takes a {@code Matcher}
218+
* over the whole list of values.
219+
* @see #header(String, Matcher)
176220
*/
177221
public static RequestMatcher header(String name, String... expectedValues) {
178222
return request -> {
@@ -185,6 +229,27 @@ public static RequestMatcher header(String name, String... expectedValues) {
185229
};
186230
}
187231

232+
/**
233+
* Assert request header, matching on the whole {@code List} of values.
234+
* <p> This can be used to check that the list has at least one value matching a
235+
* criteria ({@link Matchers#hasItem(Matcher)}), or that every value in the list
236+
* matches a common criteria ({@link Matchers#everyItem(Matcher)}), or that each
237+
* value in the list matches its corresponding dedicated criteria
238+
* ({@link Matchers#contains(Matcher[])}, and more.
239+
* @param name the name of the header to consider
240+
* @param matcher the matcher to apply to the whole list of values for that header
241+
* @since 6.0.5
242+
*/
243+
public static RequestMatcher header(String name, Matcher<? super List<String>> matcher) {
244+
return request -> {
245+
List<String> headerValues = request.getHeaders().get(name);
246+
if (headerValues == null) {
247+
fail("No header values for header [" + name + "]");
248+
}
249+
assertThat("Request header values for [" + name + "]", headerValues, matcher);
250+
};
251+
}
252+
188253
/**
189254
* Assert that the given request header does not exist.
190255
* @since 5.2

spring-test/src/test/java/org/springframework/test/web/client/match/MockRestRequestMatchersTests.java

Lines changed: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,18 +16,35 @@
1616

1717
package org.springframework.test.web.client.match;
1818

19+
import java.io.IOException;
1920
import java.net.URI;
2021
import java.util.Arrays;
2122
import java.util.Collections;
2223
import java.util.List;
2324

25+
import org.hamcrest.CoreMatchers;
26+
import org.hamcrest.Matchers;
2427
import org.junit.jupiter.api.Test;
2528

2629
import org.springframework.http.HttpMethod;
2730
import org.springframework.mock.http.client.MockClientHttpRequest;
2831

32+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
2933
import static org.assertj.core.api.Assertions.assertThatThrownBy;
34+
import static org.hamcrest.Matchers.allOf;
35+
import static org.hamcrest.Matchers.any;
36+
import static org.hamcrest.Matchers.anything;
37+
import static org.hamcrest.Matchers.contains;
38+
import static org.hamcrest.Matchers.containsInAnyOrder;
3039
import static org.hamcrest.Matchers.containsString;
40+
import static org.hamcrest.Matchers.endsWith;
41+
import static org.hamcrest.Matchers.everyItem;
42+
import static org.hamcrest.Matchers.hasItem;
43+
import static org.hamcrest.Matchers.hasSize;
44+
import static org.hamcrest.Matchers.is;
45+
import static org.hamcrest.Matchers.notNullValue;
46+
import static org.hamcrest.Matchers.nullValue;
47+
import static org.hamcrest.Matchers.startsWith;
3148

3249
/**
3350
* Unit tests for {@link MockRestRequestMatchers}.
@@ -146,6 +163,63 @@ void headerContainsWithMissingValue() {
146163
.hasMessageContaining("was \"bar\"");
147164
}
148165

166+
@Test
167+
void headerListMissing() {
168+
assertThatThrownBy(() -> MockRestRequestMatchers.header("foo", hasSize(2)).match(this.request))
169+
.isInstanceOf(AssertionError.class)
170+
.hasMessage("No header values for header [foo]");
171+
}
172+
173+
@Test
174+
void headerListMatchers() throws IOException {
175+
this.request.getHeaders().put("foo", Arrays.asList("bar", "baz"));
176+
177+
MockRestRequestMatchers.header("foo", containsInAnyOrder(endsWith("baz"), endsWith("bar"))).match(this.request);
178+
MockRestRequestMatchers.header("foo", contains(is("bar"), is("baz"))).match(this.request);
179+
MockRestRequestMatchers.header("foo", contains(is("bar"), Matchers.anything())).match(this.request);
180+
MockRestRequestMatchers.header("foo", hasItem(endsWith("baz"))).match(this.request);
181+
MockRestRequestMatchers.header("foo", everyItem(startsWith("ba"))).match(this.request);
182+
MockRestRequestMatchers.header("foo", hasSize(2)).match(this.request);
183+
184+
//these can be a bit ambiguous when reading the test (the compiler selects the list matcher):
185+
MockRestRequestMatchers.header("foo", notNullValue()).match(this.request);
186+
MockRestRequestMatchers.header("foo", is(anything())).match(this.request);
187+
MockRestRequestMatchers.header("foo", allOf(notNullValue(), notNullValue())).match(this.request);
188+
189+
//these are not as ambiguous thanks to an inner matcher that is either obviously list-oriented,
190+
//string-oriented or obviously a vararg of matchers
191+
//list matcher version
192+
MockRestRequestMatchers.header("foo", allOf(notNullValue(), hasSize(2))).match(this.request);
193+
//vararg version
194+
MockRestRequestMatchers.header("foo", allOf(notNullValue(), endsWith("ar"))).match(this.request);
195+
MockRestRequestMatchers.header("foo", is((any(String.class)))).match(this.request);
196+
MockRestRequestMatchers.header("foo", CoreMatchers.either(is("bar")).or(is(nullValue()))).match(this.request);
197+
MockRestRequestMatchers.header("foo", is(notNullValue()), is(notNullValue())).match(this.request);
198+
}
199+
200+
@Test
201+
void headerListContainsMismatch() {
202+
this.request.getHeaders().put("foo", Arrays.asList("bar", "baz"));
203+
204+
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> MockRestRequestMatchers
205+
.header("foo", contains(containsString("ba"))).match(this.request))
206+
.withMessage("Request header values for [foo]\n"
207+
+ "Expected: iterable containing [a string containing \"ba\"]\n"
208+
+ " but: not matched: \"baz\"");
209+
210+
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> MockRestRequestMatchers
211+
.header("foo", hasItem(endsWith("ba"))).match(this.request))
212+
.withMessage("Request header values for [foo]\n"
213+
+ "Expected: a collection containing a string ending with \"ba\"\n"
214+
+ " but: mismatches were: [was \"bar\", was \"baz\"]");
215+
216+
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> MockRestRequestMatchers
217+
.header("foo", everyItem(endsWith("ar"))).match(this.request))
218+
.withMessage("Request header values for [foo]\n"
219+
+ "Expected: every item is a string ending with \"ar\"\n"
220+
+ " but: an item was \"baz\"");
221+
}
222+
149223
@Test
150224
void headers() throws Exception {
151225
this.request.getHeaders().put("foo", Arrays.asList("bar", "baz"));
@@ -210,4 +284,62 @@ void queryParamContainsWithMissingValue() {
210284
.hasMessageContaining("was \"bar\"");
211285
}
212286

287+
288+
@Test
289+
void queryParamListMissing() {
290+
assertThatThrownBy(() -> MockRestRequestMatchers.queryParam("foo", hasSize(2)).match(this.request))
291+
.isInstanceOf(AssertionError.class)
292+
.hasMessage("No queryParam [foo]");
293+
}
294+
295+
@Test
296+
void queryParamListMatchers() throws IOException {
297+
this.request.setURI(URI.create("http://www.foo.example/a?foo=bar&foo=baz"));
298+
299+
MockRestRequestMatchers.queryParam("foo", containsInAnyOrder(endsWith("baz"), endsWith("bar"))).match(this.request);
300+
MockRestRequestMatchers.queryParam("foo", contains(is("bar"), is("baz"))).match(this.request);
301+
MockRestRequestMatchers.queryParam("foo", contains(is("bar"), Matchers.anything())).match(this.request);
302+
MockRestRequestMatchers.queryParam("foo", hasItem(endsWith("baz"))).match(this.request);
303+
MockRestRequestMatchers.queryParam("foo", everyItem(startsWith("ba"))).match(this.request);
304+
MockRestRequestMatchers.queryParam("foo", hasSize(2)).match(this.request);
305+
306+
//these can be a bit ambiguous when reading the test (the compiler selects the list matcher):
307+
MockRestRequestMatchers.queryParam("foo", notNullValue()).match(this.request);
308+
MockRestRequestMatchers.queryParam("foo", is(anything())).match(this.request);
309+
MockRestRequestMatchers.queryParam("foo", allOf(notNullValue(), notNullValue())).match(this.request);
310+
311+
//these are not as ambiguous thanks to an inner matcher that is either obviously list-oriented,
312+
//string-oriented or obviously a vararg of matchers
313+
//list matcher version
314+
MockRestRequestMatchers.queryParam("foo", allOf(notNullValue(), hasSize(2))).match(this.request);
315+
//vararg version
316+
MockRestRequestMatchers.queryParam("foo", allOf(notNullValue(), endsWith("ar"))).match(this.request);
317+
MockRestRequestMatchers.queryParam("foo", is((any(String.class)))).match(this.request);
318+
MockRestRequestMatchers.queryParam("foo", CoreMatchers.either(is("bar")).or(is(nullValue()))).match(this.request);
319+
MockRestRequestMatchers.queryParam("foo", is(notNullValue()), is(notNullValue())).match(this.request);
320+
}
321+
322+
@Test
323+
void queryParamListContainsMismatch() {
324+
this.request.setURI(URI.create("http://www.foo.example/a?foo=bar&foo=baz"));
325+
326+
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> MockRestRequestMatchers
327+
.queryParam("foo", contains(containsString("ba"))).match(this.request))
328+
.withMessage("Request queryParam values for [foo]\n"
329+
+ "Expected: iterable containing [a string containing \"ba\"]\n"
330+
+ " but: not matched: \"baz\"");
331+
332+
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> MockRestRequestMatchers
333+
.queryParam("foo", hasItem(endsWith("ba"))).match(this.request))
334+
.withMessage("Request queryParam values for [foo]\n"
335+
+ "Expected: a collection containing a string ending with \"ba\"\n"
336+
+ " but: mismatches were: [was \"bar\", was \"baz\"]");
337+
338+
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> MockRestRequestMatchers
339+
.queryParam("foo", everyItem(endsWith("ar"))).match(this.request))
340+
.withMessage("Request queryParam values for [foo]\n"
341+
+ "Expected: every item is a string ending with \"ar\"\n"
342+
+ " but: an item was \"baz\"");
343+
}
344+
213345
}

0 commit comments

Comments
 (0)