Search code examples
javaconcurrencyspring-webfluxmockmvc

Is MockMvc eligible for WebFlux controllers testing?


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:

  1. Sometimes Jackson complains during JSON parsing that an EOF was reached ('No content to map due to end-of-input', although in the log I can see a valid full JSON)
  2. Sometimes first test fails and second passes as if in both cases an error was returned, even though I can see in the logs that for the first test normal response was generated, not the erroneous one
  3. Sometimes second test fails and first passes as if in both cases valid JSONs where returned

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:

  1. Can MockMvc be used to test reactive controllers at all?
  2. If it can, do I do anything incorrectly?

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.


Solution

  • 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.