Spring MockRestServiceServer, обрабатывающий несколько запросов к одному и тому же URI (автообнаружение)

Допустим, я пишу интеграционные тесты Spring для службы REST A. Эта служба, в свою очередь, обращается к другой службе REST B и получает список URI для обращения к службе REST C. Это своего рода шаблон автоматического обнаружения. Я хочу смоделировать ответы B и C, используя MockRestServiceServer.
Теперь ответ от B представляет собой список URI, все они очень похожи, и для примера скажем, что мой ответ от B выглядит примерно так:

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

Просто служба A добавит каждый из них к базовому URL для службы C и выполнит эти запросы.
Дразнить В легко, так как это всего 1 запрос.
Mocking C - это хлопотно, так как я должен был бы смоделировать каждый отдельный URI, чтобы получить соответствующий ответ. Я хочу автоматизировать это!
Итак, сначала я пишу свой собственный механизм сопоставления, чтобы он соответствовал не полному URL, а его части:

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));
    }
}

Это работает нормально, так как теперь я могу сделать это:

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 */);

Теперь все, что мне нужно, - это создатель ответа, который знает полный URL-адрес запроса и где находятся макетные данные (я буду иметь их в виде файлов json в папке ресурсов тестирования):

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);
    }
}

Теперь все просто, мне нужно написать конструктор, который принимает URI запроса в виде строки и возвращает фиктивные данные в виде строки! 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());

Работает нормально! .... по первому запросу.
После первого запроса (/stuff/1.json) мой MockRestServiceServer отвечает сообщением "Ошибка подтверждения: дальнейшие запросы не ожидаются".
По сути, я могу сделать столько запросов к этому MockRestServiceServer, сколько было вызовов.expect() для него. И так как у меня было только 1 из них, только первый запрос пройдет.
Есть ли способ обойти это? Я действительно не хочу издеваться над сервисом С 10 или 20 раз...

3 ответа

Решение

Если вы посмотрите на класс MockRestServiceServer, он поддерживает два метода "wait()". Первый по умолчанию равен "ExpectedCount.once()", но второй метод позволяет вам изменить это значение

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

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

Я обнаружил, что этот тикет MockRestServiceServer должен допускать многократное ожидание, в котором описываются некоторые параметры для второго метода.

В вашем случае, я думаю, что добавление статического импорта и использование метода manyTimes() является более точным кодом, чем цикл for

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

Другие варианты

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

РЕДАКТИРОВАТЬ: см. Ответ @emeraldjava, который показывает правильное решение для пользователей Spring 4.3+.

К сожалению, не существует какого-либо приятного механизма ожидания нескольких звонков. Вы делаете это вручную или используете циклы, например:

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

Помните, что запросы должны вызываться без каких-либо прерываний, например, не может быть другого вызова REST, который не соответствует URI "/ stuff".

Для меня ситуация была немного иной. В моем тестовом классе у меня было два вспомогательных метода, которые обрабатывали запрос-заглушку. Поскольку один из этих методов находился после другого, я получил следующую ошибку: Cannot add more expectations after actual requests are made.

Что решило для меня ошибку, так это создание нового макетаockRestServiceServer в каждом из методов:

Например, у меня возникла ошибка:

      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();
}

По сути, ожидания были заявлены 2 раза в одном тестовом классе.

Что решило проблему, так это изменение этих вспомогательных методов на:

      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();
}

Другие вопросы по тегам