Spring MockRestServiceServer handling multiple requests to the same URI (auto-discovery)

Daniel Gruszczyk picture Daniel Gruszczyk · Jun 8, 2015 · Viewed 32.9k times · Source

Let's say I am writing Spring integration tests for a REST service A. This service in turn hits another REST service B and gets a list of URIs to hit on REST service C. It is kind of auto-discovery pattern. I want to mock B and C responses using MockRestServiceServer.
Now the response from B is a list of URIs, they are all very similar, and for the sake of the example lets say my response from B is like so:

{
    uris: ["/stuff/1.json", "/stuff/2.json", "/stuff/39.json", "/stuff/47.json"]
}

Simply service A will append each of them onto base URL for service C and make those requests.
Mocking B is easy since it is only 1 request.
Mocking C is a hassle as I would have to mock every single URI to appropriate mock response. I want to automate it!
So first I write my own matcher to match not a full URL, but part of it:

public class RequestContainsUriMatcher implements RequestMatcher {
    private final String uri;

    public RequestContainsUriMatcher(String uri){
        this.uri = uri;
    }

    @Override
    public void match(ClientHttpRequest clientHttpRequest) throws IOException, AssertionError {
        assertTrue(clientHttpRequest.getURI().contains(uri));
    }
}

This works fine as now I can do this:

public RequestMatcher requestContainsUri(String uri) {
    return new RequestContainsUriMatcher(uri);
}

MockRestServiceServer.createServer(restTemplate)
            .expect(requestContainsUri("/stuff"))
            .andExpect(method(HttpMethod.GET))
            .andRespond(/* I will get to response creator */);

Now all I need is a response creator that knows the full request URL and where the mock data sits (I will have it as json files in test resources folder):

public class AutoDiscoveryCannedDataResponseCreator implements ResponseCreator {
    private final Function<String, String> cannedDataBuilder;

    public AutoDiscoveryCannedDataResponseCreator(Function<String, String> cannedDataBuilder) {
        this.cannedDataBuilder = cannedDataBuilder;
    }

    @Override
    public ClientHttpResponse createResponse(ClientHttpRequest clientHttpRequest) throws IOException {
        return withSuccess(cannedDataBuilder.apply(requestUri), MediaType.APPLICATION_JSON)
                    .createResponse(clientHttpRequest);
    }
}

Now stuff is easy, I have to write a builder that takes request URI as a string and returns mock data, as a String! Brilliant!

public ResponseCreator withAutoDetectedCannedData() {
    Function<String, String> cannedDataBuilder = new Function<String, String>() {
        @Override
        public String apply(String requestUri) {
            //logic to get the canned data based on URI
            return cannedData;
        }
    };

    return new AutoDiscoveryCannedDataResponseCreator(cannedDataBuilder);
}

MockRestServiceServer.createServer(restTemplate)
            .expect(requestContainsUri("/stuff"))
            .andExpect(method(HttpMethod.GET))
            .andRespond(withAutoDetectedCannedData());

It works fine! .... For the first request.
After the first request (/stuff/1.json) my MockRestServiceServer responds with message "Assertion error: no further requests expected".
Basically, I can make as many requests to that MockRestServiceServer as there were .expect() calls on it. And since I had only 1 of them, only first request will go through.
Is there a way around it? I really don't want to mock service C 10 or 20 times...

Answer

emeraldjava picture emeraldjava · Jan 18, 2017

If you look at the MockRestServiceServer class, it supports two 'expect()' methods. The first defaults to 'ExpectedCount.once()' but the second method allows you change this value

public ResponseActions expect(RequestMatcher matcher) {
    return this.expect(ExpectedCount.once(), matcher);
}

public ResponseActions expect(ExpectedCount count, RequestMatcher matcher) {
    return this.expectationManager.expectRequest(count, matcher);
}

I found this ticket MockRestServiceServer should allow for an expectation to occur multiple times which outlines some options for second method.

In your case I think adding static import and using the manyTimes() method is neater code than the for loop

MockRestServiceServer
            .expect(manyTimes(), requestContainsUri("/stuff"))
            .andExpect(method(HttpMethod.GET))

Other options are

once();
manyTimes();
times(5);
min(2);
max(8);
between(3,6);