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

49.3k views Asked by At

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...

3

There are 3 answers

2
emeraldjava On BEST ANSWER

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);
1
rapasoft On

EDIT: See answer from @emeraldjava which shows the correct solution for Spring 4.3+ users.

Unfortunately there isn't any nice mechanism to expect multiple calls. You either do it manually or use loops, e.g.:

for (int i = 0; i < 10; i++) {           
        mockRestServiceServer
                .expect(requestContainsUri("/stuff"))
                .andExpect(method(HttpMethod.GET))
                .andRespond(withAutoDetectedCannedData());
}

Be aware that the requests must be called without any interruptions, e.g. there cannot be another REST call that doesn't match the "/stuff" URI.

0
frankmurphy On

For me the situation was a little different. In my test class I had 2 helper methods which handled the stub request. Since one of these methods was located after the other one, it I received the following error: Cannot add more expectations after actual requests are made.

What solved the error for me, was creating a new mockRestServiceServer in each of the methods:

E.g, what gave me an error was this:

public void testStubRequestMethodOne(String expectedURL){

        mockRestServiceServer.expect(requestTo(basePath + expectedURL))
                .andExpect(method(HttpMethod.POST))
                .andRespond(withSuccess());

        api.performPost(parameter1, parameter2)
        
        mockRestServiceServer.verify();

        mockRestServiceServer.reset();
}

// SERIES OF UNIT TESTS //

public void testStubRequestMethodTwo(String expectedURL, String extraURLPart){

        mockRestServiceServer.expect(requestTo(basePath + expectedURL + extraURLPart))
                .andExpect(method(HttpMethod.POST))
                .andRespond(withSuccess());

        api.performPost(parameter1, parameter2)
        
        mockRestServiceServer.verify();

        mockRestServiceServer.reset();
}

Basically, the expectations were declared 2 times in the same test class.

What solved it was changing those helper methods to:

public void testStubRequestMethodOne(String expectedURL){
        mockRestServiceServer = MockRestServiceServer.bindTo(restTemplate).build(); /*<-- this is new*/

        mockRestServiceServer.expect(requestTo(basePath + expectedURL))
                .andExpect(method(HttpMethod.POST))
                .andRespond(withSuccess());

        api.performPost(parameter1, parameter2)
        
        mockRestServiceServer.verify();

        mockRestServiceServer.reset();
}

// SERIES OF UNIT TESTS //

public void testStubRequestMethodTwo(String expectedURL, String extraURLPart){
        mockRestServiceServer = MockRestServiceServer.bindTo(restTemplate).build(); /*<-- this is new*/

        mockRestServiceServer.expect(requestTo(basePath + expectedURL + extraURLPart))
                .andExpect(method(HttpMethod.POST))
                .andRespond(withSuccess());

        api.performPost(parameter1, parameter2)
        
        mockRestServiceServer.verify();

        mockRestServiceServer.reset();
}