Skip to content

Commit 8d2bc3b

Browse files
committed
Add support for fluent preparation of the request in MockMvcTester
See gh-32913
1 parent a3e7fd4 commit 8d2bc3b

File tree

3 files changed

+306
-78
lines changed

3 files changed

+306
-78
lines changed

spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MockMvcTester.java

+158-22
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,18 @@
2323
import java.util.function.Function;
2424
import java.util.stream.StreamSupport;
2525

26+
import org.assertj.core.api.AssertProvider;
27+
28+
import org.springframework.http.HttpMethod;
2629
import org.springframework.http.MediaType;
2730
import org.springframework.http.converter.GenericHttpMessageConverter;
2831
import org.springframework.http.converter.HttpMessageConverter;
2932
import org.springframework.lang.Nullable;
33+
import org.springframework.mock.web.MockHttpServletRequest;
3034
import org.springframework.test.web.servlet.MockMvc;
3135
import org.springframework.test.web.servlet.MvcResult;
3236
import org.springframework.test.web.servlet.RequestBuilder;
37+
import org.springframework.test.web.servlet.request.AbstractMockHttpServletRequestBuilder;
3338
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
3439
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder;
3540
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
@@ -58,11 +63,25 @@
5863
* MockMvcTester mvc = MockMvcTester.of(new PersonController());
5964
* </code></pre>
6065
*
61-
* <p>Once a tester instance is available, you can perform requests in a similar
62-
* fashion as with {@link MockMvc}, and wrapping the result in
63-
* {@code assertThat()} provides access to assertions. For instance:
66+
* <p>Simple, single-statement assertions can be done wrapping the request
67+
* builder in {@code assertThat()} provides access to assertions. For instance:
6468
* <pre><code class="java">
6569
* // perform a GET on /hi and assert the response body is equal to Hello
70+
* assertThat(mvc.get().uri("/hi")).hasStatusOk().hasBodyTextEqualTo("Hello");
71+
* </code></pre>
72+
*
73+
*<p>For more complex scenarios the {@linkplain MvcTestResult result} of the
74+
* exchange can be assigned in a variable to run multiple assertions:
75+
* <pre><code class="java">
76+
* // perform a POST on /save and assert the response body is empty
77+
* MvcTestResult result = mvc.post().uri("/save").exchange();
78+
* assertThat(result).hasStatus(HttpStatus.CREATED);
79+
* assertThat(result).body().isEmpty();
80+
* </code></pre>
81+
*
82+
* <p>You can also perform requests using the static builders approach that
83+
* {@link MockMvc} uses. For instance:<pre><code class="java">
84+
* // perform a GET on /hi and assert the response body is equal to Hello
6685
* assertThat(mvc.perform(get("/hi")))
6786
* .hasStatusOk().hasBodyTextEqualTo("Hello");
6887
* </code></pre>
@@ -74,12 +93,11 @@
7493
* which allows you to assert that a request failed unexpectedly:
7594
* <pre><code class="java">
7695
* // perform a GET on /boom and assert the message for the the unresolved exception
77-
* assertThat(mvc.perform(get("/boom")))
78-
* .hasUnresolvedException())
96+
* assertThat(mvc.get().uri("/boom")).hasUnresolvedException())
7997
* .withMessage("Test exception");
8098
* </code></pre>
8199
*
82-
* <p>{@link MockMvcTester} can be configured with a list of
100+
* <p>{@code MockMvcTester} can be configured with a list of
83101
* {@linkplain HttpMessageConverter message converters} to allow the response
84102
* body to be deserialized, rather than asserting on the raw values.
85103
*
@@ -104,18 +122,17 @@ private MockMvcTester(MockMvc mockMvc, @Nullable GenericHttpMessageConverter<Obj
104122
}
105123

106124
/**
107-
* Create a {@link MockMvcTester} instance that delegates to the given
108-
* {@link MockMvc} instance.
125+
* Create an instance that delegates to the given {@link MockMvc} instance.
109126
* @param mockMvc the MockMvc instance to delegate calls to
110127
*/
111128
public static MockMvcTester create(MockMvc mockMvc) {
112129
return new MockMvcTester(mockMvc, null);
113130
}
114131

115132
/**
116-
* Create an {@link MockMvcTester} instance using the given, fully
117-
* initialized (i.e., <em>refreshed</em>) {@link WebApplicationContext}. The
118-
* given {@code customizations} are applied to the {@link DefaultMockMvcBuilder}
133+
* Create an instance using the given, fully initialized (i.e.,
134+
* <em>refreshed</em>) {@link WebApplicationContext}. The given
135+
* {@code customizations} are applied to the {@link DefaultMockMvcBuilder}
119136
* that ultimately creates the underlying {@link MockMvc} instance.
120137
* <p>If no further customization of the underlying {@link MockMvc} instance
121138
* is required, use {@link #from(WebApplicationContext)}.
@@ -134,8 +151,8 @@ public static MockMvcTester from(WebApplicationContext applicationContext,
134151
}
135152

136153
/**
137-
* Shortcut to create an {@link MockMvcTester} instance using the given,
138-
* fully initialized (i.e., <em>refreshed</em>) {@link WebApplicationContext}.
154+
* Shortcut to create an instance using the given fully initialized (i.e.,
155+
* <em>refreshed</em>) {@link WebApplicationContext}.
139156
* <p>Consider using {@link #from(WebApplicationContext, Function)} if
140157
* further customization of the underlying {@link MockMvc} instance is
141158
* required.
@@ -148,9 +165,8 @@ public static MockMvcTester from(WebApplicationContext applicationContext) {
148165
}
149166

150167
/**
151-
* Create an {@link MockMvcTester} instance by registering one or more
152-
* {@code @Controller} instances and configuring Spring MVC infrastructure
153-
* programmatically.
168+
* Create an instance by registering one or more {@code @Controller} instances
169+
* and configuring Spring MVC infrastructure programmatically.
154170
* <p>This allows full control over the instantiation and initialization of
155171
* controllers and their dependencies, similar to plain unit tests while
156172
* also making it possible to test one controller at a time.
@@ -170,8 +186,8 @@ public static MockMvcTester of(Collection<?> controllers,
170186
}
171187

172188
/**
173-
* Shortcut to create an {@link MockMvcTester} instance by registering one
174-
* or more {@code @Controller} instances.
189+
* Shortcut to create an instance by registering one or more {@code @Controller}
190+
* instances.
175191
* <p>The minimum infrastructure required by the
176192
* {@link org.springframework.web.servlet.DispatcherServlet DispatcherServlet}
177193
* to serve requests with annotated controllers is created. Consider using
@@ -187,8 +203,8 @@ public static MockMvcTester of(Object... controllers) {
187203
}
188204

189205
/**
190-
* Return a new {@link MockMvcTester} instance using the specified
191-
* {@linkplain HttpMessageConverter message converters}.
206+
* Return a new instance using the specified {@linkplain HttpMessageConverter
207+
* message converters}.
192208
* <p>If none are specified, only basic assertions on the response body can
193209
* be performed. Consider registering a suitable JSON converter for asserting
194210
* against JSON data structures.
@@ -200,8 +216,105 @@ public MockMvcTester withHttpMessageConverters(Iterable<HttpMessageConverter<?>>
200216
}
201217

202218
/**
203-
* Perform a request and return a {@link MvcTestResult result} that can be
204-
* used with standard {@link org.assertj.core.api.Assertions AssertJ} assertions.
219+
* Prepare an HTTP GET request.
220+
* <p>The returned builder can be wrapped in {@code assertThat} to enable
221+
* assertions on the result. For multi-statements assertions, use
222+
* {@linkplain MockMvcRequestBuilder#exchange() exchange} to assign the
223+
* result.
224+
* @return a request builder for specifying the target URI
225+
*/
226+
public MockMvcRequestBuilder get() {
227+
return method(HttpMethod.GET);
228+
}
229+
230+
/**
231+
* Prepare an HTTP HEAD request.
232+
* <p>The returned builder can be wrapped in {@code assertThat} to enable
233+
* assertions on the result. For multi-statements assertions, use
234+
* {@linkplain MockMvcRequestBuilder#exchange() exchange} to assign the
235+
* result.
236+
* @return a request builder for specifying the target URI
237+
*/
238+
public MockMvcRequestBuilder head() {
239+
return method(HttpMethod.HEAD);
240+
}
241+
242+
/**
243+
* Prepare an HTTP POST request.
244+
* <p>The returned builder can be wrapped in {@code assertThat} to enable
245+
* assertions on the result. For multi-statements assertions, use
246+
* {@linkplain MockMvcRequestBuilder#exchange() exchange} to assign the
247+
* result.
248+
* @return a request builder for specifying the target URI
249+
*/
250+
public MockMvcRequestBuilder post() {
251+
return method(HttpMethod.POST);
252+
}
253+
254+
/**
255+
* Prepare an HTTP PUT request.
256+
* <p>The returned builder can be wrapped in {@code assertThat} to enable
257+
* assertions on the result. For multi-statements assertions, use
258+
* {@linkplain MockMvcRequestBuilder#exchange() exchange} to assign the
259+
* result.
260+
* @return a request builder for specifying the target URI
261+
*/
262+
public MockMvcRequestBuilder put() {
263+
return method(HttpMethod.PUT);
264+
}
265+
266+
/**
267+
* Prepare an HTTP PATCH request.
268+
* <p>The returned builder can be wrapped in {@code assertThat} to enable
269+
* assertions on the result. For multi-statements assertions, use
270+
* {@linkplain MockMvcRequestBuilder#exchange() exchange} to assign the
271+
* result.
272+
* @return a request builder for specifying the target URI
273+
*/
274+
public MockMvcRequestBuilder patch() {
275+
return method(HttpMethod.PATCH);
276+
}
277+
278+
/**
279+
* Prepare an HTTP DELETE request.
280+
* <p>The returned builder can be wrapped in {@code assertThat} to enable
281+
* assertions on the result. For multi-statements assertions, use
282+
* {@linkplain MockMvcRequestBuilder#exchange() exchange} to assign the
283+
* result.
284+
* @return a request builder for specifying the target URI
285+
*/
286+
public MockMvcRequestBuilder delete() {
287+
return method(HttpMethod.DELETE);
288+
}
289+
290+
/**
291+
* Prepare an HTTP OPTIONS request.
292+
* <p>The returned builder can be wrapped in {@code assertThat} to enable
293+
* assertions on the result. For multi-statements assertions, use
294+
* {@linkplain MockMvcRequestBuilder#exchange() exchange} to assign the
295+
* result.
296+
* @return a request builder for specifying the target URI
297+
*/
298+
public MockMvcRequestBuilder options() {
299+
return method(HttpMethod.OPTIONS);
300+
}
301+
302+
/**
303+
* Prepare a request for the specified {@code HttpMethod}.
304+
* <p>The returned builder can be wrapped in {@code assertThat} to enable
305+
* assertions on the result. For multi-statements assertions, use
306+
* {@linkplain MockMvcRequestBuilder#exchange() exchange} to assign the
307+
* result.
308+
* @return a request builder for specifying the target URI
309+
*/
310+
public MockMvcRequestBuilder method(HttpMethod method) {
311+
return new MockMvcRequestBuilder(method);
312+
}
313+
314+
/**
315+
* Perform a request using {@link MockMvcRequestBuilders} and return a
316+
* {@link MvcTestResult result} that can be used with standard
317+
* {@link org.assertj.core.api.Assertions AssertJ} assertions.
205318
* <p>Use static methods of {@link MockMvcRequestBuilders} to prepare the
206319
* request, wrapping the invocation in {@code assertThat}. The following
207320
* asserts that a {@linkplain MockMvcRequestBuilders#get(URI) GET} request
@@ -226,6 +339,8 @@ public MockMvcTester withHttpMessageConverters(Iterable<HttpMessageConverter<?>>
226339
* {@link org.springframework.test.web.servlet.request.MockMvcRequestBuilders}
227340
* @return an {@link MvcTestResult} to be wrapped in {@code assertThat}
228341
* @see MockMvc#perform(RequestBuilder)
342+
* @see #get()
343+
* @see #post()
229344
*/
230345
public MvcTestResult perform(RequestBuilder requestBuilder) {
231346
Object result = getMvcResultOrFailure(requestBuilder);
@@ -259,4 +374,25 @@ private GenericHttpMessageConverter<Object> findJsonMessageConverter(
259374
.findFirst().orElse(null);
260375
}
261376

377+
378+
/**
379+
* A builder for {@link MockHttpServletRequest} that supports AssertJ.
380+
*/
381+
public final class MockMvcRequestBuilder extends AbstractMockHttpServletRequestBuilder<MockMvcRequestBuilder>
382+
implements AssertProvider<MvcTestResultAssert> {
383+
384+
private MockMvcRequestBuilder(HttpMethod httpMethod) {
385+
super(httpMethod);
386+
}
387+
388+
public MvcTestResult exchange() {
389+
return perform(this);
390+
}
391+
392+
@Override
393+
public MvcTestResultAssert assertThat() {
394+
return new MvcTestResultAssert(exchange(), MockMvcTester.this.jsonMessageConverter);
395+
}
396+
}
397+
262398
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright 2002-2024 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+
17+
package org.springframework.test.web.servlet.assertj;
18+
19+
import org.junit.jupiter.api.Test;
20+
21+
import org.springframework.beans.factory.annotation.Autowired;
22+
import org.springframework.context.annotation.Configuration;
23+
import org.springframework.context.annotation.Import;
24+
import org.springframework.http.MediaType;
25+
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
26+
import org.springframework.test.context.web.WebAppConfiguration;
27+
import org.springframework.test.web.servlet.MockMvc;
28+
import org.springframework.web.bind.annotation.GetMapping;
29+
import org.springframework.web.bind.annotation.RestController;
30+
import org.springframework.web.context.WebApplicationContext;
31+
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
32+
33+
import static org.assertj.core.api.Assertions.assertThat;
34+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
35+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
36+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
37+
38+
/**
39+
* Integration tests for {@link MockMvcTester} that use the methods that
40+
* integrate with {@link MockMvc} way of building the requests and
41+
* asserting the responses.
42+
*
43+
* @author Stephane Nicoll
44+
*/
45+
@SpringJUnitConfig
46+
@WebAppConfiguration
47+
class MockMvcTesterCompatibilityIntegrationTests {
48+
49+
private final MockMvcTester mvc;
50+
51+
MockMvcTesterCompatibilityIntegrationTests(@Autowired WebApplicationContext wac) {
52+
this.mvc = MockMvcTester.from(wac);
53+
}
54+
55+
@Test
56+
void performGet() {
57+
assertThat(this.mvc.perform(get("/greet"))).hasStatusOk();
58+
}
59+
60+
@Test
61+
void performGetWithInvalidMediaTypeAssertion() {
62+
MvcTestResult result = this.mvc.perform(get("/greet"));
63+
assertThatExceptionOfType(AssertionError.class)
64+
.isThrownBy(() -> assertThat(result).hasContentTypeCompatibleWith(MediaType.APPLICATION_JSON))
65+
.withMessageContaining("is compatible with 'application/json'");
66+
}
67+
68+
@Test
69+
void assertHttpStatusCode() {
70+
assertThat(this.mvc.get().uri("/greet")).matches(status().isOk());
71+
}
72+
73+
74+
@Configuration
75+
@EnableWebMvc
76+
@Import(TestController.class)
77+
static class WebConfiguration {
78+
}
79+
80+
@RestController
81+
static class TestController {
82+
83+
@GetMapping(path = "/greet", produces = "text/plain")
84+
String greet() {
85+
return "hello";
86+
}
87+
88+
@GetMapping(path = "/message", produces = MediaType.APPLICATION_JSON_VALUE)
89+
String message() {
90+
return "{\"message\": \"hello\"}";
91+
}
92+
}
93+
94+
}

0 commit comments

Comments
 (0)