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

匿名 (未验证) 提交于 2019-12-03 02:56:01

问题:

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

回答1:

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

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

Other options are

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


回答2:

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.



标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!