Browse Source
This commit introduces testing support for WebMvc.fn in the form of a RouterFunctionMockMvcBuilder and RouterFunctionMockMvcSpec. Closes gh-30477pull/32755/head
9 changed files with 875 additions and 24 deletions
@ -0,0 +1,100 @@
@@ -0,0 +1,100 @@
|
||||
/* |
||||
* Copyright 2002-2024 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.client; |
||||
|
||||
import org.springframework.http.converter.HttpMessageConverter; |
||||
import org.springframework.lang.Nullable; |
||||
import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; |
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders; |
||||
import org.springframework.test.web.servlet.setup.RouterFunctionMockMvcBuilder; |
||||
import org.springframework.web.servlet.HandlerExceptionResolver; |
||||
import org.springframework.web.servlet.HandlerInterceptor; |
||||
import org.springframework.web.servlet.View; |
||||
import org.springframework.web.servlet.ViewResolver; |
||||
import org.springframework.web.servlet.function.RouterFunction; |
||||
import org.springframework.web.util.pattern.PathPatternParser; |
||||
|
||||
/** |
||||
* Simple wrapper around a {@link RouterFunctionMockMvcBuilder} that implements |
||||
* {@link MockMvcWebTestClient.RouterFunctionSpec}. |
||||
* |
||||
* @author Arjen Poutsma |
||||
* @since 6.2 |
||||
*/ |
||||
class RouterFunctionMockMvcSpec extends AbstractMockMvcServerSpec<MockMvcWebTestClient.RouterFunctionSpec> |
||||
implements MockMvcWebTestClient.RouterFunctionSpec { |
||||
|
||||
private final RouterFunctionMockMvcBuilder mockMvcBuilder; |
||||
|
||||
|
||||
RouterFunctionMockMvcSpec(RouterFunction<?>... routerFunctions) { |
||||
this.mockMvcBuilder = MockMvcBuilders.routerFunctions(routerFunctions); |
||||
} |
||||
|
||||
@Override |
||||
public MockMvcWebTestClient.RouterFunctionSpec messageConverters(HttpMessageConverter<?>... messageConverters) { |
||||
this.mockMvcBuilder.setMessageConverters(messageConverters); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public MockMvcWebTestClient.RouterFunctionSpec interceptors(HandlerInterceptor... interceptors) { |
||||
mappedInterceptors(null, interceptors); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public MockMvcWebTestClient.RouterFunctionSpec mappedInterceptors(@Nullable String[] pathPatterns, HandlerInterceptor... interceptors) { |
||||
this.mockMvcBuilder.addMappedInterceptors(pathPatterns, interceptors); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public MockMvcWebTestClient.RouterFunctionSpec asyncRequestTimeout(long timeout) { |
||||
this.mockMvcBuilder.setAsyncRequestTimeout(timeout); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public MockMvcWebTestClient.RouterFunctionSpec handlerExceptionResolvers(HandlerExceptionResolver... exceptionResolvers) { |
||||
this.mockMvcBuilder.setHandlerExceptionResolvers(exceptionResolvers); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public MockMvcWebTestClient.RouterFunctionSpec viewResolvers(ViewResolver... resolvers) { |
||||
this.mockMvcBuilder.setViewResolvers(resolvers); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public MockMvcWebTestClient.RouterFunctionSpec singleView(View view) { |
||||
this.mockMvcBuilder.setSingleView(view); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
public MockMvcWebTestClient.RouterFunctionSpec patternParser(PathPatternParser parser) { |
||||
this.mockMvcBuilder.setPatternParser(parser); |
||||
return this; |
||||
} |
||||
|
||||
@Override |
||||
protected ConfigurableMockMvcBuilder<?> getMockMvcBuilder() { |
||||
return this.mockMvcBuilder; |
||||
} |
||||
} |
||||
@ -0,0 +1,321 @@
@@ -0,0 +1,321 @@
|
||||
/* |
||||
* Copyright 2002-2024 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.setup; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.Arrays; |
||||
import java.util.Collections; |
||||
import java.util.List; |
||||
import java.util.function.Supplier; |
||||
|
||||
import jakarta.servlet.ServletContext; |
||||
|
||||
import org.springframework.beans.factory.InitializingBean; |
||||
import org.springframework.context.ApplicationContext; |
||||
import org.springframework.context.ApplicationContextAware; |
||||
import org.springframework.format.support.FormattingConversionService; |
||||
import org.springframework.http.converter.HttpMessageConverter; |
||||
import org.springframework.lang.Nullable; |
||||
import org.springframework.mock.web.MockServletContext; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.web.accept.ContentNegotiationManager; |
||||
import org.springframework.web.context.WebApplicationContext; |
||||
import org.springframework.web.context.support.WebApplicationObjectSupport; |
||||
import org.springframework.web.servlet.DispatcherServlet; |
||||
import org.springframework.web.servlet.HandlerExceptionResolver; |
||||
import org.springframework.web.servlet.HandlerInterceptor; |
||||
import org.springframework.web.servlet.View; |
||||
import org.springframework.web.servlet.ViewResolver; |
||||
import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer; |
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistration; |
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; |
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; |
||||
import org.springframework.web.servlet.function.RouterFunction; |
||||
import org.springframework.web.servlet.function.support.HandlerFunctionAdapter; |
||||
import org.springframework.web.servlet.function.support.RouterFunctionMapping; |
||||
import org.springframework.web.servlet.handler.MappedInterceptor; |
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; |
||||
import org.springframework.web.servlet.resource.ResourceUrlProvider; |
||||
import org.springframework.web.servlet.view.InternalResourceViewResolver; |
||||
import org.springframework.web.util.pattern.PathPatternParser; |
||||
|
||||
/** |
||||
* A {@code MockMvcBuilder} that accepts {@link RouterFunction} registrations |
||||
* thus allowing full control over the instantiation and initialization of |
||||
* router functions and their dependencies similar to plain unit tests, and also |
||||
* making it possible to test one function at a time. |
||||
* |
||||
* <p>This builder creates the minimum infrastructure required by the |
||||
* {@link DispatcherServlet} to serve requests with router functions and |
||||
* also provides methods for customization. The resulting configuration and |
||||
* customization options are equivalent to using MVC Java config except |
||||
* using builder style methods. |
||||
* |
||||
* <p>To configure view resolution, either select a "fixed" view to use for every |
||||
* request performed (see {@link #setSingleView(View)}) or provide a list of |
||||
* {@code ViewResolver}s (see {@link #setViewResolvers(ViewResolver...)}). |
||||
* |
||||
* @author Arjen Poutsma |
||||
* @since 6.2 |
||||
*/ |
||||
public class RouterFunctionMockMvcBuilder extends AbstractMockMvcBuilder<RouterFunctionMockMvcBuilder> { |
||||
|
||||
private final RouterFunction<?> routerFunction; |
||||
|
||||
private List<HttpMessageConverter<?>> messageConverters = new ArrayList<>(); |
||||
|
||||
private final List<MappedInterceptor> mappedInterceptors = new ArrayList<>(); |
||||
|
||||
@Nullable |
||||
private List<HandlerExceptionResolver> handlerExceptionResolvers; |
||||
|
||||
@Nullable |
||||
private Long asyncRequestTimeout; |
||||
|
||||
@Nullable |
||||
private List<ViewResolver> viewResolvers; |
||||
|
||||
@Nullable |
||||
private PathPatternParser patternParser; |
||||
|
||||
|
||||
private Supplier<RouterFunctionMapping> handlerMappingFactory = RouterFunctionMapping::new; |
||||
|
||||
|
||||
protected RouterFunctionMockMvcBuilder(RouterFunction<?>... routerFunctions) { |
||||
Assert.notEmpty(routerFunctions, "RouterFunctions must not be empty"); |
||||
|
||||
this.routerFunction = Arrays.stream(routerFunctions).reduce(RouterFunction::andOther).orElseThrow(); |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Set the message converters to use in argument resolvers and in return value |
||||
* handlers, which support reading and/or writing to the body of the request |
||||
* and response. If no message converters are added to the list, a default |
||||
* list of converters is added instead. |
||||
*/ |
||||
public RouterFunctionMockMvcBuilder setMessageConverters(HttpMessageConverter<?>...messageConverters) { |
||||
this.messageConverters = Arrays.asList(messageConverters); |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Add interceptors mapped to all incoming requests. |
||||
*/ |
||||
public RouterFunctionMockMvcBuilder addInterceptors(HandlerInterceptor... interceptors) { |
||||
addMappedInterceptors(null, interceptors); |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Add interceptors mapped to a set of path patterns. |
||||
*/ |
||||
public RouterFunctionMockMvcBuilder addMappedInterceptors(@Nullable String[] pathPatterns, |
||||
HandlerInterceptor... interceptors) { |
||||
|
||||
for (HandlerInterceptor interceptor : interceptors) { |
||||
this.mappedInterceptors.add(new MappedInterceptor(pathPatterns, null, interceptor)); |
||||
} |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Set the HandlerExceptionResolver types to use as a list. |
||||
*/ |
||||
public RouterFunctionMockMvcBuilder setHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) { |
||||
this.handlerExceptionResolvers = exceptionResolvers; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Set the HandlerExceptionResolver types to use as an array. |
||||
*/ |
||||
public RouterFunctionMockMvcBuilder setHandlerExceptionResolvers(HandlerExceptionResolver... exceptionResolvers) { |
||||
this.handlerExceptionResolvers = Arrays.asList(exceptionResolvers); |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Configure factory to create a custom {@link RequestMappingHandlerMapping}. |
||||
* @param factory the factory |
||||
* @since 5.0 |
||||
*/ |
||||
public RouterFunctionMockMvcBuilder setCustomHandlerMapping(Supplier<RouterFunctionMapping> factory) { |
||||
this.handlerMappingFactory = factory; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Set up view resolution with the given {@link ViewResolver ViewResolvers}. |
||||
* If not set, an {@link InternalResourceViewResolver} is used by default. |
||||
*/ |
||||
public RouterFunctionMockMvcBuilder setViewResolvers(ViewResolver...resolvers) { |
||||
this.viewResolvers = Arrays.asList(resolvers); |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets up a single {@link ViewResolver} that always returns the provided |
||||
* view instance. This is a convenient shortcut if you need to use one |
||||
* View instance only -- e.g. rendering generated content (JSON, XML, Atom). |
||||
*/ |
||||
public RouterFunctionMockMvcBuilder setSingleView(View view) { |
||||
this.viewResolvers = Collections.<ViewResolver>singletonList(new StaticViewResolver(view)); |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Specify the timeout value for async execution. In Spring MVC Test, this |
||||
* value is used to determine how to long to wait for async execution to |
||||
* complete so that a test can verify the results synchronously. |
||||
* @param timeout the timeout value in milliseconds |
||||
*/ |
||||
public RouterFunctionMockMvcBuilder setAsyncRequestTimeout(long timeout) { |
||||
this.asyncRequestTimeout = timeout; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Enable URL path matching with parsed |
||||
* {@link org.springframework.web.util.pattern.PathPattern PathPatterns} |
||||
* instead of String pattern matching with a {@link org.springframework.util.PathMatcher}. |
||||
* @param parser the parser to use |
||||
* @since 5.3 |
||||
*/ |
||||
public RouterFunctionMockMvcBuilder setPatternParser(@Nullable PathPatternParser parser) { |
||||
this.patternParser = parser; |
||||
return this; |
||||
} |
||||
|
||||
|
||||
@Override |
||||
protected WebApplicationContext initWebAppContext() { |
||||
MockServletContext servletContext = new MockServletContext(); |
||||
StubWebApplicationContext wac = new StubWebApplicationContext(servletContext); |
||||
registerRouterFunction(wac); |
||||
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, wac); |
||||
return wac; |
||||
} |
||||
|
||||
private void registerRouterFunction(StubWebApplicationContext wac) { |
||||
HandlerFunctionConfiguration config = new HandlerFunctionConfiguration(); |
||||
config.setApplicationContext(wac); |
||||
ServletContext sc = wac.getServletContext(); |
||||
|
||||
wac.addBean("routerFunction", this.routerFunction); |
||||
|
||||
FormattingConversionService mvcConversionService = config.mvcConversionService(); |
||||
wac.addBean("mvcConversionService", mvcConversionService); |
||||
ResourceUrlProvider resourceUrlProvider = config.mvcResourceUrlProvider(); |
||||
wac.addBean("mvcResourceUrlProvider", resourceUrlProvider); |
||||
ContentNegotiationManager mvcContentNegotiationManager = config.mvcContentNegotiationManager(); |
||||
wac.addBean("mvcContentNegotiationManager", mvcContentNegotiationManager); |
||||
|
||||
RouterFunctionMapping hm = config.routerFunctionMapping(mvcConversionService, resourceUrlProvider); |
||||
if (sc != null) { |
||||
hm.setServletContext(sc); |
||||
} |
||||
hm.setApplicationContext(wac); |
||||
hm.afterPropertiesSet(); |
||||
wac.addBean("routerFunctionMapping", hm); |
||||
|
||||
HandlerFunctionAdapter ha = config.handlerFunctionAdapter(); |
||||
wac.addBean("handlerFunctionAdapter", ha); |
||||
|
||||
wac.addBean("handlerExceptionResolver", config.handlerExceptionResolver(mvcContentNegotiationManager)); |
||||
|
||||
wac.addBeans(initViewResolvers(wac)); |
||||
} |
||||
|
||||
private List<ViewResolver> initViewResolvers(WebApplicationContext wac) { |
||||
this.viewResolvers = (this.viewResolvers != null ? this.viewResolvers : |
||||
Collections.singletonList(new InternalResourceViewResolver())); |
||||
for (Object viewResolver : this.viewResolvers) { |
||||
if (viewResolver instanceof WebApplicationObjectSupport support) { |
||||
support.setApplicationContext(wac); |
||||
} |
||||
} |
||||
return this.viewResolvers; |
||||
} |
||||
|
||||
|
||||
/** Using the MVC Java configuration as the starting point for the "standalone" setup. */ |
||||
private class HandlerFunctionConfiguration extends WebMvcConfigurationSupport { |
||||
|
||||
public RouterFunctionMapping getHandlerMapping( |
||||
FormattingConversionService mvcConversionService, |
||||
ResourceUrlProvider mvcResourceUrlProvider) { |
||||
|
||||
RouterFunctionMapping handlerMapping = handlerMappingFactory.get(); |
||||
handlerMapping.setOrder(0); |
||||
handlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider)); |
||||
handlerMapping.setMessageConverters(getMessageConverters()); |
||||
if (patternParser != null) { |
||||
handlerMapping.setPatternParser(patternParser); |
||||
} |
||||
return handlerMapping; |
||||
} |
||||
|
||||
@Override |
||||
protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) { |
||||
converters.addAll(messageConverters); |
||||
} |
||||
|
||||
@Override |
||||
protected void addInterceptors(InterceptorRegistry registry) { |
||||
for (MappedInterceptor interceptor : mappedInterceptors) { |
||||
InterceptorRegistration registration = registry.addInterceptor(interceptor.getInterceptor()); |
||||
if (interceptor.getIncludePathPatterns() != null) { |
||||
registration.addPathPatterns(interceptor.getIncludePathPatterns()); |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void configureAsyncSupport(AsyncSupportConfigurer configurer) { |
||||
if (asyncRequestTimeout != null) { |
||||
configurer.setDefaultTimeout(asyncRequestTimeout); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
protected void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) { |
||||
if (handlerExceptionResolvers == null) { |
||||
return; |
||||
} |
||||
for (HandlerExceptionResolver resolver : handlerExceptionResolvers) { |
||||
if (resolver instanceof ApplicationContextAware applicationContextAware) { |
||||
ApplicationContext applicationContext = getApplicationContext(); |
||||
if (applicationContext != null) { |
||||
applicationContextAware.setApplicationContext(applicationContext); |
||||
} |
||||
} |
||||
if (resolver instanceof InitializingBean initializingBean) { |
||||
try { |
||||
initializingBean.afterPropertiesSet(); |
||||
} |
||||
catch (Exception ex) { |
||||
throw new IllegalStateException("Failure from afterPropertiesSet", ex); |
||||
} |
||||
} |
||||
exceptionResolvers.add(resolver); |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,44 @@
@@ -0,0 +1,44 @@
|
||||
/* |
||||
* Copyright 2002-2024 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.setup; |
||||
|
||||
import java.util.Locale; |
||||
|
||||
import org.springframework.lang.Nullable; |
||||
import org.springframework.web.servlet.View; |
||||
import org.springframework.web.servlet.ViewResolver; |
||||
|
||||
/** |
||||
* A {@link ViewResolver} that always returns same View. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.2 |
||||
*/ |
||||
class StaticViewResolver implements ViewResolver { |
||||
|
||||
private final View view; |
||||
|
||||
public StaticViewResolver(View view) { |
||||
this.view = view; |
||||
} |
||||
|
||||
@Override |
||||
@Nullable |
||||
public View resolveViewName(String viewName, Locale locale) { |
||||
return this.view; |
||||
} |
||||
} |
||||
@ -0,0 +1,137 @@
@@ -0,0 +1,137 @@
|
||||
/* |
||||
* Copyright 2002-2024 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 java.util.concurrent.CompletableFuture; |
||||
import java.util.function.Consumer; |
||||
|
||||
import org.junit.jupiter.api.Nested; |
||||
import org.junit.jupiter.api.Test; |
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.test.web.reactive.server.WebTestClient; |
||||
import org.springframework.test.web.servlet.client.MockMvcWebTestClient; |
||||
import org.springframework.web.servlet.function.RequestPredicates; |
||||
import org.springframework.web.servlet.function.RouterFunction; |
||||
import org.springframework.web.servlet.function.ServerResponse; |
||||
|
||||
import static org.hamcrest.Matchers.equalTo; |
||||
import static org.springframework.web.servlet.function.RouterFunctions.route; |
||||
import static org.springframework.web.servlet.function.ServerResponse.ok; |
||||
|
||||
/** |
||||
* MockMvcTestClient equivalent of the MockMvc |
||||
* {@link org.springframework.test.web.servlet.samples.standalone.RouterFunctionTests}. |
||||
* |
||||
* @author Arjen Poutsma |
||||
*/ |
||||
public class RouterFunctionTests { |
||||
|
||||
@Test |
||||
void json() { |
||||
execute("/person/Lee", body -> body.jsonPath("$.name").isEqualTo("Lee") |
||||
.jsonPath("$.age").isEqualTo(42) |
||||
.jsonPath("$.age").value(equalTo(42)) |
||||
.jsonPath("$.age").value(Float.class, equalTo(42.0f))); |
||||
} |
||||
|
||||
@Test |
||||
public void queryParameter() { |
||||
execute("/search?name=George", body -> body.jsonPath("$.name").isEqualTo("George")); |
||||
} |
||||
|
||||
|
||||
@Nested |
||||
class AsyncTests { |
||||
|
||||
@Test |
||||
void completableFuture() { |
||||
execute("/async/completableFuture", body -> body.json("{\"name\":\"Joe\",\"age\":0}")); |
||||
} |
||||
|
||||
@Test |
||||
void publisher() { |
||||
execute("/async/publisher", body -> body.json("{\"name\":\"Joe\",\"age\":0}")); |
||||
} |
||||
|
||||
} |
||||
|
||||
|
||||
private void execute(String uri, Consumer<WebTestClient.BodyContentSpec> assertions) { |
||||
RouterFunction<?> testRoute = testRoute(); |
||||
assertions.accept(MockMvcWebTestClient.bindToRouterFunction(testRoute).build() |
||||
.get() |
||||
.uri(uri) |
||||
.accept(MediaType.APPLICATION_JSON) |
||||
.exchange() |
||||
.expectStatus().isOk() |
||||
.expectHeader().contentType(MediaType.APPLICATION_JSON) |
||||
.expectBody()); |
||||
} |
||||
|
||||
private static RouterFunction<?> testRoute() { |
||||
return route() |
||||
.GET("/person/{name}", request -> { |
||||
Person person = new Person(request.pathVariable("name")); |
||||
person.setAge(42); |
||||
return ok().body(person); |
||||
}) |
||||
.GET("/search", request -> { |
||||
String name = request.param("name").orElseThrow(NullPointerException::new); |
||||
Person person = new Person(name); |
||||
return ok().body(person); |
||||
}) |
||||
.path("/async", b -> b |
||||
.GET("/completableFuture", request -> { |
||||
CompletableFuture<Person> future = new CompletableFuture<>(); |
||||
future.complete(new Person("Joe")); |
||||
return ok().body(future); |
||||
}) |
||||
.GET("/publisher", request -> { |
||||
Mono<Person> mono = Mono.just(new Person("Joe")); |
||||
return ok().body(mono); |
||||
}) |
||||
) |
||||
.route(RequestPredicates.all(), request -> ServerResponse.notFound().build()) |
||||
.build(); |
||||
} |
||||
|
||||
@SuppressWarnings("unused") |
||||
private static class Person { |
||||
|
||||
private final String name; |
||||
|
||||
private int age; |
||||
|
||||
public Person(String name) { |
||||
this.name = name; |
||||
} |
||||
|
||||
public String getName() { |
||||
return this.name; |
||||
} |
||||
|
||||
public int getAge() { |
||||
return this.age; |
||||
} |
||||
|
||||
public void setAge(int age) { |
||||
this.age = age; |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,167 @@
@@ -0,0 +1,167 @@
|
||||
/* |
||||
* Copyright 2002-2024 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.standalone; |
||||
|
||||
import java.util.concurrent.CompletableFuture; |
||||
|
||||
import org.junit.jupiter.api.BeforeEach; |
||||
import org.junit.jupiter.api.Nested; |
||||
import org.junit.jupiter.api.Test; |
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.test.web.servlet.MockMvc; |
||||
import org.springframework.test.web.servlet.MvcResult; |
||||
import org.springframework.web.servlet.function.RequestPredicates; |
||||
import org.springframework.web.servlet.function.RouterFunction; |
||||
import org.springframework.web.servlet.function.ServerResponse; |
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8; |
||||
import static org.hamcrest.Matchers.containsString; |
||||
import static org.hamcrest.Matchers.equalTo; |
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; |
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; |
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; |
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; |
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request; |
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
||||
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.routerFunctions; |
||||
import static org.springframework.web.servlet.function.RouterFunctions.route; |
||||
import static org.springframework.web.servlet.function.ServerResponse.ok; |
||||
|
||||
/** |
||||
* @author Arjen Poutsma |
||||
*/ |
||||
public class RouterFunctionTests { |
||||
|
||||
private MockMvc mockMvc; |
||||
|
||||
@BeforeEach |
||||
void setUp() { |
||||
RouterFunction<?> testRoute = testRoute(); |
||||
this.mockMvc = routerFunctions(testRoute).defaultResponseCharacterEncoding(UTF_8).build(); |
||||
} |
||||
|
||||
@Test |
||||
void json() throws Exception { |
||||
this.mockMvc |
||||
// We use a name containing an umlaut to test UTF-8 encoding for the request and the response.
|
||||
.perform(get("/person/Jürgen").characterEncoding(UTF_8).accept(MediaType.APPLICATION_JSON)) |
||||
.andExpect(status().isOk()) |
||||
.andExpect(content().contentType("application/json")) |
||||
.andExpect(content().encoding(UTF_8)) |
||||
.andExpect(content().string(containsString("Jürgen"))) |
||||
.andExpect(jsonPath("$.name").value("Jürgen")) |
||||
.andExpect(jsonPath("$.age").value(42)) |
||||
.andExpect(jsonPath("$.age").value(42.0f)) |
||||
.andExpect(jsonPath("$.age").value(equalTo(42))) |
||||
.andExpect(jsonPath("$.age").value(equalTo(42.0f), Float.class)) |
||||
.andExpect(jsonPath("$.age", equalTo(42))) |
||||
.andExpect(jsonPath("$.age", equalTo(42.0f), Float.class)); |
||||
} |
||||
|
||||
@Test |
||||
public void queryParameter() throws Exception { |
||||
this.mockMvc |
||||
.perform(get("/search?name=George").accept(MediaType.APPLICATION_JSON)) |
||||
.andExpect(status().isOk()) |
||||
.andExpect(content().contentType("application/json")) |
||||
.andExpect(jsonPath("$.name").value("George")); |
||||
} |
||||
|
||||
@Nested |
||||
class AsyncTests { |
||||
|
||||
@Test |
||||
void completableFuture() throws Exception { |
||||
MvcResult mvcResult = mockMvc.perform(get("/async/completableFuture")) |
||||
.andExpect(request().asyncStarted()) |
||||
.andReturn(); |
||||
|
||||
mockMvc.perform(asyncDispatch(mvcResult)) |
||||
.andExpect(status().isOk()) |
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON)) |
||||
.andExpect(content().string("{\"name\":\"Joe\",\"age\":0}")); |
||||
} |
||||
|
||||
@Test |
||||
void publisher() throws Exception { |
||||
MvcResult mvcResult = mockMvc.perform(get("/async/publisher")) |
||||
.andExpect(request().asyncStarted()) |
||||
.andReturn(); |
||||
|
||||
mockMvc.perform(asyncDispatch(mvcResult)) |
||||
.andExpect(status().isOk()) |
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON)) |
||||
.andExpect(content().string("{\"name\":\"Joe\",\"age\":0}")); |
||||
|
||||
} |
||||
|
||||
} |
||||
|
||||
|
||||
private static RouterFunction<?> testRoute() { |
||||
return route() |
||||
.GET("/person/{name}", request -> { |
||||
Person person = new Person(request.pathVariable("name")); |
||||
person.setAge(42); |
||||
return ok().body(person); |
||||
}) |
||||
.GET("/search", request -> { |
||||
String name = request.param("name").orElseThrow(NullPointerException::new); |
||||
Person person = new Person(name); |
||||
return ok().body(person); |
||||
}) |
||||
.path("/async", b -> b |
||||
.GET("/completableFuture", request -> { |
||||
CompletableFuture<Person> future = new CompletableFuture<>(); |
||||
future.complete(new Person("Joe")); |
||||
return ok().body(future); |
||||
}) |
||||
.GET("/publisher", request -> { |
||||
Mono<Person> mono = Mono.just(new Person("Joe")); |
||||
return ok().body(mono); |
||||
}) |
||||
) |
||||
.route(RequestPredicates.all(), request -> ServerResponse.notFound().build()) |
||||
.build(); |
||||
} |
||||
|
||||
@SuppressWarnings("unused") |
||||
private static class Person { |
||||
|
||||
private final String name; |
||||
|
||||
private int age; |
||||
|
||||
public Person(String name) { |
||||
this.name = name; |
||||
} |
||||
|
||||
public String getName() { |
||||
return this.name; |
||||
} |
||||
|
||||
public int getAge() { |
||||
return this.age; |
||||
} |
||||
|
||||
public void setAge(int age) { |
||||
this.age = age; |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue