spring-projects / spring-framework

Spring Framework
https://spring.io/projects/spring-framework
Apache License 2.0
56.7k stars 38.14k forks source link

Using MockMvc to test Flux<ServerSentEvent> endpoints? #22544

Closed kdvolder closed 5 years ago

kdvolder commented 5 years ago

I'm strugling with writing tests for rest endpoints that return Flux. I'd like to use mockMvc for this. Is this even possible?

Note that, allthough I have some rest endpoints returning Flux and/or Mono I am not using WebFllux but just regular MVC. The framework handles those types fine and for my Flux example, these are received on the browser side as a 'stream' of ServerSentEvents. This all works fine.

The trouble is. I can't seem to find a good way to write tests agains these endpoints using mockmvc.

The best I came up with is here:

https://github.com/kdvolder/mvc-flux-testing/blob/master/src/test/java/com/example/demo/MvcFluxDemoApplicationTests.java

Is turning the stream of events into String via `getResponse().getContentAsString()' really the best I can do here? (Note: @rstoyanchev has told me it is, mockMvc doesn't provide good ways to test this sort of thing. He recommends to use WebTestClient).

So I submit for consideration the following:

  1. It may be useful to expand mockMvc apis to handle endpoints returning streamed ServerSentEvents better. Perhaps, something that can get the content as a Flux? Then we can use reactive testing support such as StepVerifier to validate the content in our tests.

  2. Add a pointer in the docs on using WebTestClient instead for these scenarios.

kdvolder commented 5 years ago

Note: I don't seem to be the only one strugling with this. See: https://stackoverflow.com/questions/35499655/how-to-test-server-sent-events-with-spring

There are two answers there, neither of which seem very satisfactory.

rstoyanchev commented 5 years ago

Thanks Kris. Indeed WebTestClient is the best answer we have for this. Building something (async, streaming parsing) in MockMvc would be non-trivial. We certainly can and need to explain the available options in the documentation however.

daliborfilus commented 3 years ago

How can we use WebTestClient with Spring Boot MVC (starter-web)? It throws unsatisfied dependency on me. WebTestClient is in reactive namespaced package. So we cannot use it for regular MVC? (I tried @AutoConfigureWebTestClient, but that didn't help; even the doc for that annotations says it's only for WebFlux.)

So if WebTestClient is for WebFlux only, and MockMvc is for MVC only, and we want to test Flux/Mono responses, what should we do? I'm trying to find an answer to this for multiple hours at this point... and each of such answers is contradictory or confusing or incorrect, because things have changed.

rstoyanchev commented 3 years ago

@daliborfilus, indeed WebTestClient uses WebClient from spring-webflux internally but this is a fully expected supported scenario in Spring Boot, just like it is fully supported to use the WebClient in a Spring Boot application with WebMvc. These are the rules Boot uses to determine the web environment.

In addition in 5.3 we updated WebTestClient to support both WebFlux and WebMvc controllers so it can be used for both. You might want to review the updated reference in the WebTestClient and the MockMvc sections.

That said, for testing SSE and streaming responses, full end-to-end tests through the WebTestClient along with the StepVerifier from Project Reactor is your best bet.

daliborfilus commented 3 years ago

@rstoyanchev Thank you. Seem like I tried to use WebTestClient as autowired component, which doesn't work in this case? (I'm using Spring Boot 2.4.3, which uses Spring Web 5.3.4.)

The WebTestClient link you mention links to sample project https://github.com/spring-projects/spring-framework/tree/master/spring-test/src/test/java/org/springframework/test/web/client/samples which doesn't use WebTestClient, at least not directly? One of the tests mocks the server side completely, which I don't want - I just want to mock the servlet part and still do end-to-end integration test. (Or even expose random port and test it using rest template - which is maybe what this sample project does? But that's another topic.)

The part 3.6.1 - Setup explains how to create instance of WebTestClient - or more precisely MockMvcWebTestClient - bound to a controller or the application context. I don't want to instantiate the controller here, because then I would need to pass all autowired dependencies to it, so the ApplicationContext is preferrable I think.

So following this, I now have this code:

@ActiveProfiles("test")
// tried with RANDOM_PORT and MOCK
@SpringBootTest(classes = [BackendApplication::class], webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc(printOnlyOnFailure = false)
@AutoConfigureRestDocs(outputDir = "build/generated-snippets")
internal class MonitoringControllerTest {

    @Autowired
    lateinit var context: WebApplicationContext

    lateinit var client: WebTestClient

    @BeforeEach
    fun setUp() {
        client = WebTestClient.bindToApplicationContext(context).build()
    }

    @Test
    fun indexAcceptJsonAndDocument() {
        client.get().uri("$contextPath/entities/export")
//                .with(SecurityMockMvcRequestPostProcessors.user("user").authorities(allAuthorities())) // FIXME: find replacement for this
            .accept(MediaType.APPLICATION_JSON)
            .exchange()
            .expectStatus().isOk
            .expectBody()
            .jsonPath("$.totalElements").isEqualTo(3)
            .jsonPath("$.totalPages").isEqualTo(1)
//            .andDo(MockMvcRestDocumentation.document("monitored-entities-export")) // FIXME: find replacement for this to get restdocs working; use consumeWith?
    }
}

which throws and I don't know why:

org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'webHandler' available
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanDefinition(DefaultListableBeanFactory.java:863)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getMergedLocalBeanDefinition(AbstractBeanFactory.java:1344)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:309)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:213)
    at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1159)
    at org.springframework.web.server.adapter.WebHttpHandlerBuilder.applicationContext(WebHttpHandlerBuilder.java:165)
    at org.springframework.test.web.reactive.server.ApplicationContextSpec.initHttpHandlerBuilder(ApplicationContextSpec.java:43)
    at org.springframework.test.web.reactive.server.AbstractMockServerSpec.configureClient(AbstractMockServerSpec.java:86)
    at org.springframework.test.web.reactive.server.AbstractMockServerSpec.build(AbstractMockServerSpec.java:107)
    at app.web.data.monitoring.MonitoringControllerTest.setUp(MonitoringControllerTest.kt:35)

Some relevant links: https://stackoverflow.com/questions/60792015/springboot-test-fails-with-no-bean-named-webhandler-available

Tried adding @AutoConfigureWebTestClient - didn't help. Tried adding dependency on starter-webflux along with AutoConfigureWebTestClient - didn't help.

daliborfilus commented 3 years ago

@rstoyanchev Uh. I tried to at least solve the todos in the comments above (mock user, apply rest docs) and I found out that I cannot use .apply(springSecurity()) in the test because of #20606 - kotlin cannot infer the type T. I would like to supply the the type to the apply method, but I cannot, because the type is package-private - and I can see you are the author of that class (spring-test-5.3.4-sources.jar!/org/springframework/test/web/reactive/server/ApplicationContextSpec.java).

So even if I could get that "webHandler" dependency error go away, I still cannot use the WebTestClient in Kotlin.

rstoyanchev commented 3 years ago

As you already found out, you need to use MockMvcWebTestClient. That has a method to bind to an ApplicationContext as well so change to the following:

MockMvcWebTestClient.bindToApplicationContext(this.wac).build();

As for #20606 yes that is an issue. I believe there is some effort to resolve it but I don't have the details.

daliborfilus commented 3 years ago

@rstoyanchev Thank you, I will stick to MVC + MockMvc for now and we'll see in the future.

rstoyanchev commented 3 years ago

No problem. For streaming and Server-Sent Events, you'll need the WebTestClient which can also do full end-to-end integration tests but apart from that you can also stick to MockMvc.