-
Notifications
You must be signed in to change notification settings - Fork 38.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support in MockRestServiceServer
to assert ALL header
and queryParam
values with a single Matcher
#28660
Comments
Hi @mspiess,
That's a good point. In fact, it almost appears that the current API would support that. At least, the current tests in place are based on that assumption. Lines 126 to 131 in 93b340e
That Since In other words, rewriting the assertion as follows would also cause the test to pass. MockRestRequestMatchers.header("foo", containsString("bar")).match(this.request); In order to perform an assertion on each header value, you have to provide a matcher for each value. Thus, the following fails: MockRestRequestMatchers.header("foo", containsString("bar"), containsString("bar")).match(this.request); Whereas, the following pass: MockRestRequestMatchers.header("foo", containsString("bar"), containsString("baz")).match(this.request);
MockRestRequestMatchers.header("foo", containsString("ba"), containsString("ba")).match(this.request); Thus, there is in fact support for performing assertions against all of the values for a given header, but it is awkward to use and not really documented. Furthermore, with the current implementation of this feature you have to know exactly how many values are expected and you have to repeat the FYI: this applies to all variants of One way to address this would be to introduce new variants of these methods that accept a single public static RequestMatcher header(String name, Matcher<? super String> matcher) {
return request -> {
List<String> headerValues = request.getHeaders().get(name);
assertNotNull("No header values", headerValues);
String reason = "Request header [" + name + "]";
headerValues.forEach(value -> assertThat(reason, value, matcher));
};
} This would achieve what you are requesting; however, there is a downside: any invocation of |
Hi @sbrannen, Perhaps a new overload would be a less of a breaking change: public static RequestMatcher header(String name, Matcher<? super Collection<? super String>> matcher) {
return request -> {
List<String> headerValues = request.getHeaders().get(name);
assertThat("Request header [" + name + "]", headerValues, matcher);
};
} This would allow the following: MockRestRequestMatchers.header("foo", everyItem(containsString("ba"))).match(this.request); This brings more control to the call site, as the caller can decide how to match the collection. E.g. one may want to assert that any value matches, which is not possible currently without knowing the amount and order of the header values.
I suggest |
When it comes to headers, I don't think the expectation that you would know how many and match each individually is all that unreasonable. I'd expect that matching across all headers in one condition is probably a less common case. The Javadoc on the existing methods could certainly use an improvement, even if it's obvious that a vararg means, one value or Matcher for each value. Nothing wrong with introducing a variant that accepts a |
I agree.
Yes, let's improve the documentation for the affected methods.
I did some research on what's available in Hamcrest. Instead of The interesting (disappointing?) part is that This makes Though you can create a custom @SuppressWarnings({ "rawtypes", "unchecked" })
public static <U> Matcher<Iterable<? super U>> everyItem(Matcher<U> itemMatcher) {
return new Every(itemMatcher);
}
With the proposed signature below, we don't actually run into any issues with the compiler (in terms of source compatibility). public static RequestMatcher header(String name, Matcher<Iterable<? super String>> matcher) So we could choose to keep the |
Keeping the |
It turns out that there are open issues for Hamcrest on this topic. See hamcrest/JavaHamcrest#289 (comment) for details. |
@sbrannen, do we go with your most recent proposal? public static RequestMatcher header(String name, Matcher<Iterable<? super String>> matcher) Its usefulness is reduced by the Hamcrest issue, however, that doesn't seem to be moving. Perhaps we can just go back to the earlier suggestion with a slightly different name: public static RequestMatcher everyHeader(String name, Matcher<? super String> matcher); |
MockRestServiceServer
to assert ALL header
and queryParam
values with an single Matcher
MockRestServiceServer
to assert ALL header
and queryParam
values with an single Matcher
MockRestServiceServer
to assert ALL header
and queryParam
values with a single Matcher
Yes, I believe that's the route we need to go.
Indeed, unfortunately.
I think this is too limiting. The same matcher would be applied to each queryParam/header value individually instead of acting on the collection as a whole -- effectively the same semantics as In other words, if we go this route users would not be able to benefit from features like Of course, if we wanted to provide a workaround for the generics issues in Hamcrest... we could introduce both methods. However, I'm not convinced we should do that; as I mentioned in #28660 (comment), users can come up with their own workarounds for the generics issues in Hamcrest. @simonbasle has expressed in interest in taking on this issue, so I've assigned it to him for tentative inclusion in 6.0.5. Please note that I changed the title of this issue to reflect a changed focus on both |
Very much worth explaining and illustrating this in the Javadoc of such a new method. |
I'm having a look and I need to better understand why, but it appears that the following signature works both for public static RequestMatcher header(String name, Matcher<? super List<String>> matcher) { See test below demonstrating usage with various matchers@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()));
} |
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. Closes spring-projectsgh-28660
Superseded by gh-29953 That PR will allow to do something like this: .andExpect(header("Accept", everyItem(endsWith("json")))) |
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 changes the name of two recently introduced methods in the `MockRestRequestMatchers` class for header and queryParam. These have been found to cause false negatives in user tests, due to the new overload taking precedence in some cases. Namely, using a `Matcher` factory method which can apply to both `List` and `String` will cause the compiler to select the newest list overload, by instantiating a `Matcher<Object>`. This can cause false negatives in user tests, failing tests that used to pass because the Matcher previously applied to the first String in the header or queryParam value list. For instance, `equalsTo("a")`. The new overloads are recent enough and this has enough potential to cause an arbitrary number of user tests to fail that we break the API to eliminate the ambiguity, by renaming the methods with a `*List` suffix. Closes spring-projectsgh-30220 Close spring-projectsgh-30238 See spring-projectsgh-29953 See spring-projectsgh-28660
This commit changes the name of two recently introduced methods in the `MockRestRequestMatchers` class for header and queryParam. These have been found to cause false negatives in user tests, due to the new overload taking precedence in some cases. Namely, using a `Matcher` factory method which can apply to both `List` and `String` will cause the compiler to select the newest list overload, by instantiating a `Matcher<Object>`. This can cause false negatives in user tests, failing tests that used to pass because the Matcher previously applied to the first String in the header or queryParam value list. For instance, `equalsTo("a")`. The new overloads are recent enough and this has enough potential to cause an arbitrary number of user tests to fail that we break the API to eliminate the ambiguity, by renaming the methods with a `*List` suffix. Closes gh-30220 Closes gh-30238 See gh-29953 See gh-28660
Affects: 5.3.21
The current API for
MockRestServiceServer
andMockRestRequestMatchers
does not allow a test to validate that all values of a certain header key satisfy a matcher. Trying to dodoes not work because it passes if the first value for the header ends with
"json"
. I would like to be able to do something like this:or something to that effect.
The text was updated successfully, but these errors were encountered: