Интеграционное тестирование Spring SseEmitters

Я искал подсказки о том, как лучше всего тестировать методы Spring MVC Controller, которые возвращают SseEmitters. Я пришел довольно коротко, но у меня есть решение проб и ошибок, которое тестирует асинхронное многопоточное поведение. Ниже приведен пример кода только для демонстрации концепции, может быть опечатка или две:

Класс контроллера:

@Autowired
Publisher<MyResponse> responsePublisher;

@RequestMapping("/mypath")
public SseEmitter index() throws IOException {
    SseEmitter emitter = new SseEmitter();
    Observable<MyResponse> responseObservable = RxReactiveStreams.toObservable(responsePublisher);

    responseObservable.subscribe(
            response -> {
                try {
                    emitter.send(response);
               } catch (IOException ex) {
                    emitter.completeWithError(ex);
               }
            },
            error -> {
                emitter.completeWithError(error);
            },
            emitter::complete
    );

    return emitter;
}

Тестовый класс:

//A threaded dummy publisher to demonstrate async properties.
//Sends 2 responses with a 250ms pause in between.
protected static class MockPublisher implements Publisher<MyResponse> {
    @Override
    public void subscribe(Subscriber<? super MyResponse> subscriber) {
        new Thread() {
            @Override
            public void run() {
                try {
                    subscriber.onNext(response1);
                    Thread.sleep(250);
                    subscriber.onNext(response2);
                } catch (InterruptedException ex) {
                }
                subscriber.onComplete();
            }
        }.start();
    }
}

//Assume @Configuration that autowires the above mock publisher in the controller.

//Tests the output of the controller method.
@Test
public void testSseEmitter() throws Exception {
    String path = "http://localhost/mypath/";
    String expectedContent = "data:" + response1.toString() + "\n\n" +
                             "data:" + response2.toString() + "\n\n");

    //Trial-and-Error attempts at testing this SseEmitter mechanism have yielded the following:
    //- Returning an SseEmitter triggers 'asyncStarted'
    //- Calling 'asyncResult' forces the test to wait for the process to complete
    //- However, there is no actual 'asyncResult' to test.  Instead, the content is checked for the published data.
    mockMvc.perform(get(path).contentType(MediaType.ALL))
        .andExpect(status().isOk())
        .andExpect(request().asyncStarted())
        .andExpect(request().asyncResult(nullValue()))
        .andExpect(header().string("Content-Type", "text/event-stream"))
        .andExpect(content().string(expectedContent))
}

Как отмечено в комментариях, asyncResult() вызывается, чтобы гарантировать, что издатель завершит свою работу и отправит оба ответа до завершения теста. Без него проверка содержимого завершается ошибкой из-за того, что в содержимом присутствует только один ответ. Однако фактического результата для проверки нет, поэтому asyncResult имеет значение null.

Мой конкретный вопрос заключается в том, есть ли лучший и более точный способ заставить тест ждать завершения асинхронного процесса, а не метод klugie здесь для ожидания несуществующего asyncResult. Мой более широкий вопрос заключается в том, существуют ли другие библиотеки или методы Spring, которые лучше подходят для этого по сравнению с этими асинхронными функциями. Спасибо!


person Kevin Page    schedule 10.05.2016    source источник
comment
Я не знаю, есть ли лучший способ, но ваш кладж помог мне протестировать контроллер, который возвращал SseEmitter, используя обычный Spring MVC вместо Spring WebFlux - так что спасибо!   -  person Erin Drummond    schedule 05.06.2019
comment
Я думаю, что это более надежный способ контролировать поведение на стороне сервера: stackoverflow.com/a/51564293/3640794   -  person xmcax    schedule 11.09.2020


Ответы (1)


Это более общий ответ, поскольку он предназначен для тестирования SseEmitter, который будет работать вечно, но отключится от потока SSE по истечении заданного времени ожидания.

Что касается подхода, отличного от MVC, как прокомментировал @ErinDrummond OP, вы можете изучить WebFlux.

Это минимальный пример. Возможно, вы захотите расширить заголовки запроса, разные сопоставители или, возможно, поработать над выходным потоком отдельно.

Он устанавливает отложенный поток для отключения от потока SSE, что позволит выполнять утверждения.

@Autowired
MockMvc mockMvc;


@Test
public void testSseEmitter(){

    ScheduledExecutorService execService = Executors.newScheduledThreadPool(1);
    String streamUri = "/your-get-uri");
    long timeout = 500L;
    TimeUnit timeUnit = TimeUnit.MILLISECONDS;

    MvcResult result = mockMvc.perform(get(streamURI)
            .andExpect(request().asyncStarted()).andReturn();

    MockAsyncContext asyncContext = (MockAsyncContext) result.getRequest().getAsyncContext();
    execService.schedule(() -> {
        for (AsyncListener listener : asyncContext.getListeners())
            try {
                listener.onTimeout(null);
            } catch (IOException e) {
                e.printStackTrace();
            }
    }, timeout, timeUnit);

    result.getAsyncResult();

    // assertions, e.g. response body as string contains "xyz"
    mvc.perform(asyncDispatch(result)).andExpect(content().string(containsString("xyz")));
}
person p m    schedule 22.10.2020
comment
Мне жаль. Но мне все равно этот тест не подходит. Когда я сделаю дальнейшее утверждение .andExpect(status().isOk()) в //утверждении в конце, я получу неуспешный тест, потому что он возвращает статус 503 вместо 200 (хорошо). Кроме того, как я могу издеваться над содержащимся в нем утверждением, которое он ожидает (например, xyz)? - person webMac; 26.07.2021