Unit-testing a Spring WebFlux streaming controller
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