I have a simple WebFlux application (that uses controllers, not router functions). The only non-standard part is that it uses Server-Sent-Events.
An interesting part of the controller is
@GetMapping(path = "/persons", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<Object>> persons() {
return service.persons()
.map(this::personToSse)
.onErrorResume(e -> Mono.just(throwableToSse(e)));
}
private ServerSentEvent<Object> personToSse(Person person) {
return ServerSentEvent.builder().data(person).build();
}
Service:
public interface Service {
Flux<Person> persons();
}
I have two tests:
@SpringBootTest(classes = Config.class)
@AutoConfigureMockMvc
class PersonsControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private Service service;
@Test
void streamsPersons() throws Exception {
when(service.persons())
.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);
}
@Test
void handlesExceptionDuringStreaming() throws Exception {
when(service.persons())
.thenReturn(Flux.error(new RuntimeException("Oops!")));
String responseText = mockMvc.perform(get("/persons").accept(MediaType.TEXT_EVENT_STREAM))
.andExpect(status().is2xxSuccessful())
.andReturn()
.getResponse()
.getContentAsString();
assertThat(responseText, is("event:internal-error\ndata:Oops!\n\n"));
}
First test checks that for the 'sunny day scenario' we get two persons that we expect. Second test checks what happens when an exception occurs.
The tests work perfectly when I run them one by one. But when I run them both, sometimes they pass, sometimes one of them fail, sometimes both fail. Failure reasons are different:
So it looks like there is some concurrency problem. But my test code is simple enough, it does not use any concurrency-related concepts.
The following test fails 100% of times on my machine (it just runs these 2 tests repeatedly 1000 times):
@Test
void manyTimes() throws Exception {
for (int i = 0; i < 1000; i++) {
streamsPersons();
handlesExceptionDuringStreaming();
}
}
The questions follow:
MockMvc
be used to test reactive controllers at all?Here is the full project source code: https://github.com/rpuch/sse-webflux-tests
The manyTests()
method is commented out and has to be re-enabled to be used.
1. Can MockMvc be used to test reactive controllers at all?
The answer is no, MockMvc is a blocking mockClient that will call your method once and return. It does not have the ability to read items consecutively as they get emitted The client you need to use is the Spring WebClient
.
You can read more here how to go about testing infinite streams using the Spring WebTestClient
.
2. If it can, do I do anything incorrectly?
See the answer to question one.