diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/AsyncTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/AsyncTests.java index fa118ffabd5..77259b29648 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/AsyncTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/AsyncTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,6 +32,7 @@ import org.springframework.test.web.servlet.client.MockMvcWebTestClient; import org.springframework.util.concurrent.ListenableFuture; import org.springframework.util.concurrent.ListenableFutureTask; import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; @@ -99,7 +100,7 @@ public class AsyncTests { } @Test - public void deferredResultWithImmediateValue() throws Exception { + public void deferredResultWithImmediateValue() { this.testClient.get() .uri("/1?deferredResultWithImmediateValue=true") .exchange() @@ -109,7 +110,7 @@ public class AsyncTests { } @Test - public void deferredResultWithDelayedError() throws Exception { + public void deferredResultWithDelayedError() { this.testClient.get() .uri("/1?deferredResultWithDelayedError=true") .exchange() @@ -118,7 +119,7 @@ public class AsyncTests { } @Test - public void listenableFuture() throws Exception { + public void listenableFuture() { this.testClient.get() .uri("/1?listenableFuture=true") .exchange() @@ -142,17 +143,17 @@ public class AsyncTests { @RequestMapping(path = "/{id}", produces = "application/json") private static class AsyncController { - @RequestMapping(params = "callable") + @GetMapping(params = "callable") public Callable getCallable() { return () -> new Person("Joe"); } - @RequestMapping(params = "streaming") + @GetMapping(params = "streaming") public StreamingResponseBody getStreaming() { return os -> os.write("name=Joe".getBytes(StandardCharsets.UTF_8)); } - @RequestMapping(params = "streamingSlow") + @GetMapping(params = "streamingSlow") public StreamingResponseBody getStreamingSlow() { return os -> { os.write("name=Joe".getBytes()); @@ -166,41 +167,41 @@ public class AsyncTests { }; } - @RequestMapping(params = "streamingJson") + @GetMapping(params = "streamingJson") public ResponseEntity getStreamingJson() { return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON) .body(os -> os.write("{\"name\":\"Joe\",\"someDouble\":0.5}".getBytes(StandardCharsets.UTF_8))); } - @RequestMapping(params = "deferredResult") + @GetMapping(params = "deferredResult") public DeferredResult getDeferredResult() { DeferredResult result = new DeferredResult<>(); delay(100, () -> result.setResult(new Person("Joe"))); return result; } - @RequestMapping(params = "deferredResultWithImmediateValue") + @GetMapping(params = "deferredResultWithImmediateValue") public DeferredResult getDeferredResultWithImmediateValue() { DeferredResult result = new DeferredResult<>(); result.setResult(new Person("Joe")); return result; } - @RequestMapping(params = "deferredResultWithDelayedError") + @GetMapping(params = "deferredResultWithDelayedError") public DeferredResult getDeferredResultWithDelayedError() { DeferredResult result = new DeferredResult<>(); delay(100, () -> result.setErrorResult(new RuntimeException("Delayed Error"))); return result; } - @RequestMapping(params = "listenableFuture") + @GetMapping(params = "listenableFuture") public ListenableFuture getListenableFuture() { ListenableFutureTask futureTask = new ListenableFutureTask<>(() -> new Person("Joe")); delay(100, futureTask); return futureTask; } - @RequestMapping(params = "completableFutureWithImmediateValue") + @GetMapping(params = "completableFutureWithImmediateValue") public CompletableFuture getCompletableFutureWithImmediateValue() { CompletableFuture future = new CompletableFuture<>(); future.complete(new Person("Joe")); diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/SseTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/SseTests.java new file mode 100644 index 00000000000..a22cd2a1b86 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/SseTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.test.web.servlet.samples.client.standalone; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +import org.springframework.test.web.Person; +import org.springframework.test.web.reactive.server.FluxExchangeResult; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.servlet.client.MockMvcWebTestClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import static java.time.Duration.ofMillis; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * SSE controller tests with MockMvc and WebTestClient. + * + * @author Rossen Stoyanchev + */ +public class SseTests { + + private final WebTestClient testClient = + MockMvcWebTestClient.bindToController(new SseController()).build(); + + + @Test + public void sse() { + FluxExchangeResult exchangeResult = this.testClient.get() + .uri("/persons") + .exchange() + .expectStatus().isOk() + .expectHeader().contentType("text/event-stream") + .returnResult(Person.class); + + StepVerifier.create(exchangeResult.getResponseBody()) + .expectNext(new Person("N0"), new Person("N1"), new Person("N2")) + .expectNextCount(4) + .consumeNextWith(person -> assertThat(person.getName()).endsWith("7")) + .thenCancel() + .verify(); + } + + + @RestController + private static class SseController { + + @GetMapping(path = "/persons", produces = "text/event-stream") + public Flux getPersonStream() { + return Flux.interval(ofMillis(100)).take(50).onBackpressureBuffer(50) + .map(index -> new Person("N" + index)); + } + } + +} diff --git a/src/docs/asciidoc/testing.adoc b/src/docs/asciidoc/testing.adoc index 724dd4fa16b..b497edbe3aa 100644 --- a/src/docs/asciidoc/testing.adoc +++ b/src/docs/asciidoc/testing.adoc @@ -7471,12 +7471,35 @@ or reactive type such as Reactor `Mono`: [[spring-mvc-test-vs-streaming-response]] ===== Streaming Responses -There are no options built into Spring MVC Test for container-less testing of streaming -responses. However you can test streaming requests through the <>. -This is also supported in Spring Boot where you can -{doc-spring-boot}/html/spring-boot-features.html#boot-features-testing-spring-boot-applications-testing-with-running-server[test a running server] -with `WebTestClient`. One extra advantage is the ability to use the `StepVerifier` from -project Reactor that allows declaring expectations on a stream of data. +The best way to test streaming responses such as Server-Sent Events is through the +<> which can be used as a test client to connect to a `MockMvc` instance +to perform tests on Spring MVC controllers without a running server. For example: + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + WebTestClient client = MockMvcWebTestClient.bindToController(new SseController()).build(); + + FluxExchangeResult exchangeResult = client.get() + .uri("/persons") + .exchange() + .expectStatus().isOk() + .expectHeader().contentType("text/event-stream") + .returnResult(Person.class); + + // Use StepVerifier from Project Reactor to test the streaming response + + StepVerifier.create(exchangeResult.getResponseBody()) + .expectNext(new Person("N0"), new Person("N1"), new Person("N2")) + .expectNextCount(4) + .consumeNextWith(person -> assertThat(person.getName()).endsWith("7")) + .thenCancel() + .verify(); +---- + +`WebTestClient` can also connect to a live server and perform full end-to-end integration +tests. This is also supported in Spring Boot where you can +{doc-spring-boot}/html/spring-boot-features.html#boot-features-testing-spring-boot-applications-testing-with-running-server[test a running server]. [[spring-mvc-test-server-filters]]