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