MockRestServiceServer how to verify only request path exlcuding query string
Asked Answered
D

3

9

I have a @Component DataClientImpl that calls a REST API using RestTemplate. The API endpoint has query params, which are passed when calling RestTemplate. There is a @RestClientTest Test class DataApiClientImplTest testing the DataClientImpl, mocking the REST calls using MockRestServiceServer.

In the test method I want to verify that the correct endpoint path and query params (name particularly) are used in API call. Using MockRestRequestMatchers.requestTo() and MockRestRequestMatchers.queryParam() methods to verify.

The MockRestRequestMatchers.requestTo() fails when the test is run. It appears to compare the actual url include query string with expected url without query string (that is passed to MockRestRequestMatchers.requestTo() method.

In the pom, I am using

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.6.0</version>
    <relativePath/> 
</parent>

Code below.

@RestClientTest(DataApiClientImpl.class)
@AutoConfigureWebClient(registerRestTemplate = true)
class DataApiClientImplTest {

    private static final String OBJECT_ID_URI = "http://localhost:8080/testBucketId/objects/test-obj-id";

    @Autowired
    private DataApiClientImpl dataApiClientImpl;

    @Autowired
    private MockRestServiceServer mockRestServiceServer;

    @Test
    void testApiCall() {
        mockRestServiceServer.expect(MockRestRequestMatchers.requestTo(OBJECT_ID_URI))
                .andExpect(MockRestRequestMatchers.method(HttpMethod.GET))
                .andExpect(MockRestRequestMatchers.queryParam("keyid", CoreMatchers.anything()))
                .andExpect(MockRestRequestMatchers.queryParam("token", CoreMatchers.anything()))
                .andRespond(MockRestResponseCreators.withSuccess("dummy", MediaType.APPLICATION_JSON));

        String response = dataApiClientImpl.getItem("asdf12345", "test-obj-id");

        Assertions.assertThat(response).isNotNull();
    }

}

The error is

java.lang.AssertionError: Unexpected request expected:<http://localhost:8080/testBucketId/objects/test-obj-id> but was:<http://localhost:8080/testBucketId/objects/test-obj-id?token=asdf12345&keyid=testKeyId>
Expected :http://localhost:8080/testBucketId/objects/test-obj-id
Actual   :http://localhost:8080/testBucketId/objects/test-obj-id?token=asdf12345&keyid=testKeyId

I can pass in a Matcher instead to requestTo() method. But then what is the use of MockRestRequestMatchers.queryParam() method, when the whole url with query params has to be verified using Matcher.

One option is to use startsWith Matcher MockRestRequestMatchers.requestTo(CoreMatchers.startsWith(OBJECT_ID_URI)). But it is not same as exact match verification.

Tried with other overloaded versions of MockRestRequestMatchers.requestTo() method. They all behave same.

Is there other better way to do exact endpoint path only match.

I did not include the test subject class code DataClientImpl as I thought it is out of scope of the problem, but I can add here if need.

Daleth answered 30/11, 2021 at 0:14 Comment(0)
D
10

Just in case anyone stumbles upon this thread. The answer is very simple, but adding more details of my interesting findings that might be helpful to others.

TL;DR answer:
Use mockRestServiceServer.expect(request -> assertEquals(OBJECT_ID_URI, request.getURI().getPath()))

Interesting Findings:
It appears that, it is by design the MockRestRequestMatchers.requestTo(String uri) matches against the uri including query string. Below is the implementation extract of this method,

    public static RequestMatcher requestTo(String expectedUri) {
        Assert.notNull(expectedUri, "'uri' must not be null");
        return request -> assertEquals("Request URI", expectedUri, request.getURI().toString());
    }

What I mean by design is, the java.net.URI by definition is implementation of RFC 2396 and RFC 2732. Below is the extract from java.net.URI javadoc

Represents a Uniform Resource Identifier (URI) reference. Aside from some minor deviations noted below, an instance of this class represents a URI reference as defined by RFC 2396: Uniform Resource Identifiers (URI): Generic Syntax, amended by RFC 2732: Format for Literal IPv6 Addresses in URLs.

I have not read the RFCs completely, but further the javadoc mentions under URI syntax and components

A hierarchical URI is subject to further parsing according to the syntax

[scheme:][//authority][path][?query][#fragment]

which indicates the URI is not just the request path, rather includes all these parts. Therefore the RequestMatcher returned by RequestMatchers.requestTo(String uri) method that asserts as below

assertEquals("Request URI", expectedUri, request.getURI().toString())

compares actual URI including query string against expected uri. When expected uri string is path only without query string and the actual request contains query parameters, the assert fails.

So, my intention to match only the request path in expect() method, as the query parameters are asserted with subsequent andExpect() methods can be implemented as below

mockRestServiceServer.expect(request -> assertEquals(OBJECT_ID_URI, request.getURI().getPath()))

Using java.net.URI#getPath() method, that gives only the path part of the actual URI.

Another important point here is, the RequestMatcher is a Functional Interface with method

void match(ClientHttpRequest request) throws IOException, AssertionError;

A lambda expression can be passed to expect() method that takes ClientHttpRequest as argument and returns nothing (technically void).

The test method in complete is below and works as expected

    @Test
    void testApiCall() {
        mockRestServiceServer.expect(request -> assertEquals(OBJECT_ID_URI, request.getURI().getPath()))
                .andExpect(MockRestRequestMatchers.queryParam("keyid", CoreMatchers.anything()))
                .andExpect(MockRestRequestMatchers.queryParam("token", CoreMatchers.anything()))
                .andRespond(MockRestResponseCreators.withSuccess("dummy", MediaType.APPLICATION_JSON));

        String response = dataApiClientImpl.getItem("asdf12345", "test-obj-id");

        Assertions.assertThat(response).isNotNull();
    }

Daleth answered 9/12, 2021 at 1:26 Comment(0)
N
0

I was facing a similiar issue recently and did not find any solution with built in matchers/methods. I ended up writing my own Matcher implementation, which looked something like this:

public class RequestPathMatcher implements RequestMatcher {
    private final String expectedRequestPath;

    public static RequestPathMatcher of(String expectedRequestPath) {
        return new RequestContainsUriMatcher(expectedRequestPath);
    }

    private RequestPathMatcher(String expectedRequestPath) {
        this.expectedRequestPath = expectedRequestPath;
    }

    @Override
    public void match(ClientHttpRequest clientHttpRequest) throws AssertionError {
        final var actualRequestPath = clientHttpRequest.getURI().toString();
        var matchResult = false;
        // your match logic here
        assertTrue(matchResult , String.format("Expected %s to have request path%s", actualRequestPath, expectedRequestPath));
    }
}

You can the use it like this:

 mockServer
      .expect(RequestPathMatcher.of(requestPath))
      .andRespond(...)
Neumark answered 30/11, 2021 at 8:9 Comment(4)
@madhav-turangi: any feedback?Neumark
The actual problem is with getURI().toString() method. The MockRestRequestMatchers.requestTo() pretty much does the same.Daleth
Yes, but the point is that you can implement arbitrary logic inside the overriden match method, whatever you need.Neumark
I understand the point. I replied to usage of getURI().toString() in your answer. Misunderstanding the matcher given by MockRestRequestMatchers.requestTo() was the root of the problem, mainly with URI. If using Java 8 and above, a lambda does the job, which I discovered and added more details below. Thanks for your participation.Daleth
L
0

To match the exact URL path but not the query parameters, you can use Matchers.startsWith and match up to and including the ?:

mockRestServiceServer.expect(requestTo(startsWith("http://servername/path1/path2?")))

Here is how this would look in the originally posted example:

@RestClientTest(DataApiClientImpl.class)
@AutoConfigureWebClient(registerRestTemplate = true)
class DataApiClientImplTest {

    // Include '?' at the end of the expected URL
    private static final String OBJECT_ID_URI = "http://localhost:8080/testBucketId/objects/test-obj-id?";

    @Autowired
    private DataApiClientImpl dataApiClientImpl;

    @Autowired
    private MockRestServiceServer mockRestServiceServer;

    @Test
    void testApiCall() {
        // Use startsWith to match the entire URL before query parameters (including '?')
        // Including the '?' ensures there are no unexpected path segments in the actual URL
        mockRestServiceServer.expect(MockRestRequestMatchers.requestTo(Matchers.startsWith(OBJECT_ID_URI)))
                .andExpect(MockRestRequestMatchers.method(HttpMethod.GET))
                // Query parameters can then be matched individually and regardless of order in URL
                .andExpect(MockRestRequestMatchers.queryParam("keyid", CoreMatchers.anything()))
                .andExpect(MockRestRequestMatchers.queryParam("token", CoreMatchers.anything()))
                .andRespond(MockRestResponseCreators.withSuccess("dummy", MediaType.APPLICATION_JSON));

        String response = dataApiClientImpl.getItem("asdf12345", "test-obj-id");

        Assertions.assertThat(response).isNotNull();
    }

}

Lucho answered 25/8, 2024 at 14:56 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.