Skip to content

Commit 4982b5f

Browse files
committed
Improve docs on SSE tests for Spring MVC
Closes gh-26687
1 parent f7678cd commit 4982b5f

File tree

3 files changed

+114
-19
lines changed

3 files changed

+114
-19
lines changed

spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/AsyncTests.java

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -32,6 +32,7 @@
3232
import org.springframework.util.concurrent.ListenableFuture;
3333
import org.springframework.util.concurrent.ListenableFutureTask;
3434
import org.springframework.web.bind.annotation.ExceptionHandler;
35+
import org.springframework.web.bind.annotation.GetMapping;
3536
import org.springframework.web.bind.annotation.RequestMapping;
3637
import org.springframework.web.bind.annotation.ResponseStatus;
3738
import org.springframework.web.bind.annotation.RestController;
@@ -99,7 +100,7 @@ public void deferredResult() {
99100
}
100101

101102
@Test
102-
public void deferredResultWithImmediateValue() throws Exception {
103+
public void deferredResultWithImmediateValue() {
103104
this.testClient.get()
104105
.uri("/1?deferredResultWithImmediateValue=true")
105106
.exchange()
@@ -109,7 +110,7 @@ public void deferredResultWithImmediateValue() throws Exception {
109110
}
110111

111112
@Test
112-
public void deferredResultWithDelayedError() throws Exception {
113+
public void deferredResultWithDelayedError() {
113114
this.testClient.get()
114115
.uri("/1?deferredResultWithDelayedError=true")
115116
.exchange()
@@ -118,7 +119,7 @@ public void deferredResultWithDelayedError() throws Exception {
118119
}
119120

120121
@Test
121-
public void listenableFuture() throws Exception {
122+
public void listenableFuture() {
122123
this.testClient.get()
123124
.uri("/1?listenableFuture=true")
124125
.exchange()
@@ -142,17 +143,17 @@ public void completableFutureWithImmediateValue() throws Exception {
142143
@RequestMapping(path = "/{id}", produces = "application/json")
143144
private static class AsyncController {
144145

145-
@RequestMapping(params = "callable")
146+
@GetMapping(params = "callable")
146147
public Callable<Person> getCallable() {
147148
return () -> new Person("Joe");
148149
}
149150

150-
@RequestMapping(params = "streaming")
151+
@GetMapping(params = "streaming")
151152
public StreamingResponseBody getStreaming() {
152153
return os -> os.write("name=Joe".getBytes(StandardCharsets.UTF_8));
153154
}
154155

155-
@RequestMapping(params = "streamingSlow")
156+
@GetMapping(params = "streamingSlow")
156157
public StreamingResponseBody getStreamingSlow() {
157158
return os -> {
158159
os.write("name=Joe".getBytes());
@@ -166,41 +167,41 @@ public StreamingResponseBody getStreamingSlow() {
166167
};
167168
}
168169

169-
@RequestMapping(params = "streamingJson")
170+
@GetMapping(params = "streamingJson")
170171
public ResponseEntity<StreamingResponseBody> getStreamingJson() {
171172
return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON)
172173
.body(os -> os.write("{\"name\":\"Joe\",\"someDouble\":0.5}".getBytes(StandardCharsets.UTF_8)));
173174
}
174175

175-
@RequestMapping(params = "deferredResult")
176+
@GetMapping(params = "deferredResult")
176177
public DeferredResult<Person> getDeferredResult() {
177178
DeferredResult<Person> result = new DeferredResult<>();
178179
delay(100, () -> result.setResult(new Person("Joe")));
179180
return result;
180181
}
181182

182-
@RequestMapping(params = "deferredResultWithImmediateValue")
183+
@GetMapping(params = "deferredResultWithImmediateValue")
183184
public DeferredResult<Person> getDeferredResultWithImmediateValue() {
184185
DeferredResult<Person> result = new DeferredResult<>();
185186
result.setResult(new Person("Joe"));
186187
return result;
187188
}
188189

189-
@RequestMapping(params = "deferredResultWithDelayedError")
190+
@GetMapping(params = "deferredResultWithDelayedError")
190191
public DeferredResult<Person> getDeferredResultWithDelayedError() {
191192
DeferredResult<Person> result = new DeferredResult<>();
192193
delay(100, () -> result.setErrorResult(new RuntimeException("Delayed Error")));
193194
return result;
194195
}
195196

196-
@RequestMapping(params = "listenableFuture")
197+
@GetMapping(params = "listenableFuture")
197198
public ListenableFuture<Person> getListenableFuture() {
198199
ListenableFutureTask<Person> futureTask = new ListenableFutureTask<>(() -> new Person("Joe"));
199200
delay(100, futureTask);
200201
return futureTask;
201202
}
202203

203-
@RequestMapping(params = "completableFutureWithImmediateValue")
204+
@GetMapping(params = "completableFutureWithImmediateValue")
204205
public CompletableFuture<Person> getCompletableFutureWithImmediateValue() {
205206
CompletableFuture<Person> future = new CompletableFuture<>();
206207
future.complete(new Person("Joe"));
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright 2002-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.test.web.servlet.samples.client.standalone;
17+
18+
import org.junit.jupiter.api.Test;
19+
import reactor.core.publisher.Flux;
20+
import reactor.test.StepVerifier;
21+
22+
import org.springframework.test.web.Person;
23+
import org.springframework.test.web.reactive.server.FluxExchangeResult;
24+
import org.springframework.test.web.reactive.server.WebTestClient;
25+
import org.springframework.test.web.servlet.client.MockMvcWebTestClient;
26+
import org.springframework.web.bind.annotation.GetMapping;
27+
import org.springframework.web.bind.annotation.RestController;
28+
29+
import static java.time.Duration.ofMillis;
30+
import static org.assertj.core.api.Assertions.assertThat;
31+
32+
/**
33+
* SSE controller tests with MockMvc and WebTestClient.
34+
*
35+
* @author Rossen Stoyanchev
36+
*/
37+
public class SseTests {
38+
39+
private final WebTestClient testClient =
40+
MockMvcWebTestClient.bindToController(new SseController()).build();
41+
42+
43+
@Test
44+
public void sse() {
45+
FluxExchangeResult<Person> exchangeResult = this.testClient.get()
46+
.uri("/persons")
47+
.exchange()
48+
.expectStatus().isOk()
49+
.expectHeader().contentType("text/event-stream")
50+
.returnResult(Person.class);
51+
52+
StepVerifier.create(exchangeResult.getResponseBody())
53+
.expectNext(new Person("N0"), new Person("N1"), new Person("N2"))
54+
.expectNextCount(4)
55+
.consumeNextWith(person -> assertThat(person.getName()).endsWith("7"))
56+
.thenCancel()
57+
.verify();
58+
}
59+
60+
61+
@RestController
62+
private static class SseController {
63+
64+
@GetMapping(path = "/persons", produces = "text/event-stream")
65+
public Flux<Person> getPersonStream() {
66+
return Flux.interval(ofMillis(100)).take(50).onBackpressureBuffer(50)
67+
.map(index -> new Person("N" + index));
68+
}
69+
}
70+
71+
}

src/docs/asciidoc/testing.adoc

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7471,12 +7471,35 @@ or reactive type such as Reactor `Mono`:
74717471
[[spring-mvc-test-vs-streaming-response]]
74727472
===== Streaming Responses
74737473

7474-
There are no options built into Spring MVC Test for container-less testing of streaming
7475-
responses. However you can test streaming requests through the <<WebTestClient>>.
7476-
This is also supported in Spring Boot where you can
7477-
{doc-spring-boot}/html/spring-boot-features.html#boot-features-testing-spring-boot-applications-testing-with-running-server[test a running server]
7478-
with `WebTestClient`. One extra advantage is the ability to use the `StepVerifier` from
7479-
project Reactor that allows declaring expectations on a stream of data.
7474+
The best way to test streaming responses such as Server-Sent Events is through the
7475+
<<WebTestClient>> which can be used as a test client to connect to a `MockMvc` instance
7476+
to perform tests on Spring MVC controllers without a running server. For example:
7477+
7478+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
7479+
.Java
7480+
----
7481+
WebTestClient client = MockMvcWebTestClient.bindToController(new SseController()).build();
7482+
7483+
FluxExchangeResult<Person> exchangeResult = client.get()
7484+
.uri("/persons")
7485+
.exchange()
7486+
.expectStatus().isOk()
7487+
.expectHeader().contentType("text/event-stream")
7488+
.returnResult(Person.class);
7489+
7490+
// Use StepVerifier from Project Reactor to test the streaming response
7491+
7492+
StepVerifier.create(exchangeResult.getResponseBody())
7493+
.expectNext(new Person("N0"), new Person("N1"), new Person("N2"))
7494+
.expectNextCount(4)
7495+
.consumeNextWith(person -> assertThat(person.getName()).endsWith("7"))
7496+
.thenCancel()
7497+
.verify();
7498+
----
7499+
7500+
`WebTestClient` can also connect to a live server and perform full end-to-end integration
7501+
tests. This is also supported in Spring Boot where you can
7502+
{doc-spring-boot}/html/spring-boot-features.html#boot-features-testing-spring-boot-applications-testing-with-running-server[test a running server].
74807503

74817504

74827505
[[spring-mvc-test-server-filters]]

0 commit comments

Comments
 (0)