Spring WebFlux makes it easy to write a streaming endpoint for your application. But how do you test it?

The controller

We’ll test the following controller created in the previous article:

@RestController
public class StreamingController {
    @Autowired
    private final PersonService personService;

    @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<ServerSentEvent<Object>> stream() {
        return personSevice.personDtosFlux()
                .map(this::dtoToSse)
                .onErrorResume(e -> Mono.just(throwableToSse(e)));
    }

    private ServerSentEvent<Object> dtoToSse(PersonDto dto) {
        return ServerSentEvent.builder()
                .data(dto)
                .build();
    }

    private ServerSentEvent<Object> throwableToSse(Throwable e) {
        return ServerSentEvent.builder().event("internal-error")
                .data(e.getMessage())
                .build();
    }
}

MockMvc? (nope)

The first thing you could try is using the good old MockMvc:

@WebMvcTest
@ContextConfiguration(...)
class StreamingControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private PersonServiceService service;

    @Test
    void streamsPersons() throws Exception {
        when(service.personDtosFlux())
                .thenReturn(Flux.just(new Person("John", "Smith"), new Person("Jane", "Doe")));

        String responseText = mockMvc.perform(get("/persons").accept(MediaType.TEXT_EVENT_STREAM))
                .andExpect(status().is2xxSuccessful())
                .andExpect(content().string(not(isEmptyString())))
                .andReturn()
                .getResponse()
                .getContentAsString();

        assertThatJohnAndJaneAreReturned(responseText);
    }
}

With one test method, this seems to work. But if you have another test method, weird things start happening. More details in the StackOverflow question Long story short: MockMvc should not be used to test WebFlux endpoints using streaming.

WebTestClient

Ok, there is a reactive analogue: WebTestClient.

@WebFluxTest
@ContextConfiguration(classes = TestConfiguration.class)
class StreamingControllerTest {
    private WebTestClient webClient;

    @MockBean
    private PersonService service;


    @BeforeEach
    void initClient() {
        webClient = WebTestClient.bindToController(new StreamingController(personService)).build();
    }

    @Test
    void streamsPersons() {
        when(service.personDtosFlux())
                .thenReturn(Flux.just(new Person("John", "Smith"), new Person("Jane", "Doe")));

        List<PersonDto> dtos = webClient.get().uri("/stream")
                .accept(MediaType.TEXT_EVENT_STREAM)
                .exchange()
                .expectStatus().is2xxSuccessful()
                .expectBodyList(PersonDto.class)
                .returnResult()
                .getResponseBody();

        assertThatFirstAndSecondAreReturned(dtos);
    }

    @Test
    void whenAnExceptionOccursDuringStreaming_thenItShouldBeHandled() {
        when(service.personDtosFlux())
                .thenReturn(Flux.error(new RuntimeException("Oops!")));

        String responseText = webClient.get().uri("/stream")
                .accept(MediaType.TEXT_EVENT_STREAM)
                .exchange()
                .expectStatus().is2xxSuccessful()
                .expectBody(String.class)
                .returnResult()
                .getResponseBody();

        assertThat(responseText, is("event:internal-error\ndata:Oops!\n\n"));
    }
}

One note on WebFluxTest configuration. @WebFluxTest accepts controller class name. It seems cleaner to specify the controller class in the annotation and then inject WebTestClient automatically with @Autowired instead of creating it by hand in a set-up method. But the controller class specified via @WebFluxTest actually works as a filter, so, for it to work, the controller must already be in the application context configured for the test run. For me, it seems too overcomplicated to put a controller class in a context just to have a possibility to inject it back. And if you have autoscan here, you probably put a lot of containers to the test context, which, again, seems to contradict the simplicity required by unit-testing a single controller.

On the other hand, if we just configure the WebTestClient manually in 2 lines of code (could be 1 line if you like), we get a clear vision about how the controller is connected to the WebTestClient.

Conclusion

A concise way to test WebFlux streaming endpoints in unit tests has been demonstrated.

P.S. Why do we use that ServerSentEvent in the controller? Please see the previous article on handling errors with SSE in Spring WebFlux