Add testing support for WebMvc.fn
This commit introduces testing support for WebMvc.fn in the form of a RouterFunctionMockMvcBuilder and RouterFunctionMockMvcSpec. Closes gh-30477
This commit is contained in:
committed by
Arjen Poutsma
parent
d708343a05
commit
8f3b7484e2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
@@ -35,6 +35,7 @@ import org.springframework.test.web.servlet.ResultHandler;
|
||||
import org.springframework.test.web.servlet.ResultMatcher;
|
||||
import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcConfigurer;
|
||||
import org.springframework.test.web.servlet.setup.RouterFunctionMockMvcBuilder;
|
||||
import org.springframework.test.web.servlet.setup.StandaloneMockMvcBuilder;
|
||||
import org.springframework.validation.Validator;
|
||||
import org.springframework.web.accept.ContentNegotiationManager;
|
||||
@@ -47,6 +48,7 @@ import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import org.springframework.web.servlet.LocaleResolver;
|
||||
import org.springframework.web.servlet.View;
|
||||
import org.springframework.web.servlet.ViewResolver;
|
||||
import org.springframework.web.servlet.function.RouterFunction;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||
import org.springframework.web.util.pattern.PathPatternParser;
|
||||
|
||||
@@ -81,13 +83,25 @@ public interface MockMvcWebTestClient {
|
||||
* Begin creating a {@link WebTestClient} by providing the {@code @Controller}
|
||||
* instance(s) to handle requests with.
|
||||
* <p>Internally this is delegated to and equivalent to using
|
||||
* {@link org.springframework.test.web.servlet.setup.MockMvcBuilders#standaloneSetup(Object...)}.
|
||||
* {@link org.springframework.test.web.servlet.setup.MockMvcBuilders#standaloneSetup(Object...)}
|
||||
* to initialize {@link MockMvc}.
|
||||
*/
|
||||
static ControllerSpec bindToController(Object... controllers) {
|
||||
return new StandaloneMockMvcSpec(controllers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin creating a {@link WebTestClient} by providing the {@link RouterFunction}
|
||||
* instance(s) to handle requests with.
|
||||
* <p>Internally this is delegated to and equivalent to using
|
||||
* {@link org.springframework.test.web.servlet.setup.MockMvcBuilders#routerFunctions(RouterFunction[])}
|
||||
* to initialize {@link MockMvc}.
|
||||
* @since 6.2
|
||||
*/
|
||||
static RouterFunctionSpec bindToRouterFunction(RouterFunction<?>... routerFunctions) {
|
||||
return new RouterFunctionMockMvcSpec(routerFunctions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin creating a {@link WebTestClient} by providing a
|
||||
* {@link WebApplicationContext} with Spring MVC infrastructure and
|
||||
@@ -381,4 +395,72 @@ public interface MockMvcWebTestClient {
|
||||
ControllerSpec customHandlerMapping(Supplier<RequestMappingHandlerMapping> factory);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Specification for configuring {@link MockMvc} to test one or more
|
||||
* {@linkplain RouterFunction router functions}
|
||||
* directly, and a simple facade around {@link RouterFunctionMockMvcBuilder}.
|
||||
* @since 6.2
|
||||
*/
|
||||
interface RouterFunctionSpec extends MockMvcServerSpec<RouterFunctionSpec> {
|
||||
|
||||
/**
|
||||
* Set the message converters to use.
|
||||
* <p>This is delegated to
|
||||
* {@link RouterFunctionMockMvcBuilder#setMessageConverters(HttpMessageConverter[])}.
|
||||
*/
|
||||
RouterFunctionSpec messageConverters(HttpMessageConverter<?>... messageConverters);
|
||||
|
||||
/**
|
||||
* Add global interceptors.
|
||||
* <p>This is delegated to
|
||||
* {@link RouterFunctionMockMvcBuilder#addInterceptors(HandlerInterceptor...)}.
|
||||
*/
|
||||
RouterFunctionSpec interceptors(HandlerInterceptor... interceptors);
|
||||
|
||||
/**
|
||||
* Add interceptors for specific patterns.
|
||||
* <p>This is delegated to
|
||||
* {@link RouterFunctionMockMvcBuilder#addMappedInterceptors(String[], HandlerInterceptor...)}.
|
||||
*/
|
||||
RouterFunctionSpec mappedInterceptors(
|
||||
@Nullable String[] pathPatterns, HandlerInterceptor... interceptors);
|
||||
|
||||
/**
|
||||
* Specify the timeout value for async execution.
|
||||
* <p>This is delegated to
|
||||
* {@link RouterFunctionMockMvcBuilder#setAsyncRequestTimeout(long)}.
|
||||
*/
|
||||
RouterFunctionSpec asyncRequestTimeout(long timeout);
|
||||
|
||||
/**
|
||||
* Set the HandlerExceptionResolver types to use.
|
||||
* <p>This is delegated to
|
||||
* {@link RouterFunctionMockMvcBuilder#setHandlerExceptionResolvers(HandlerExceptionResolver...)}.
|
||||
*/
|
||||
RouterFunctionSpec handlerExceptionResolvers(HandlerExceptionResolver... exceptionResolvers);
|
||||
|
||||
/**
|
||||
* Set up view resolution.
|
||||
* <p>This is delegated to
|
||||
* {@link RouterFunctionMockMvcBuilder#setViewResolvers(ViewResolver...)}.
|
||||
*/
|
||||
RouterFunctionSpec viewResolvers(ViewResolver... resolvers);
|
||||
|
||||
/**
|
||||
* Set up a single {@link ViewResolver} with a fixed view.
|
||||
* <p>This is delegated to
|
||||
* {@link RouterFunctionMockMvcBuilder#setSingleView(View)}.
|
||||
*/
|
||||
RouterFunctionSpec singleView(View view);
|
||||
|
||||
/**
|
||||
* Enable URL path matching with parsed
|
||||
* {@link org.springframework.web.util.pattern.PathPattern PathPatterns}.
|
||||
* <p>This is delegated to
|
||||
* {@link RouterFunctionMockMvcBuilder#setPatternParser(PathPatternParser)}.
|
||||
*/
|
||||
RouterFunctionSpec patternParser(PathPatternParser parser);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* 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.
|
||||
@@ -19,6 +19,7 @@ package org.springframework.test.web.servlet.setup;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MockMvcBuilder;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
import org.springframework.web.servlet.function.RouterFunction;
|
||||
|
||||
/**
|
||||
* The main class to import in order to access all available {@link MockMvcBuilder MockMvcBuilders}.
|
||||
@@ -76,4 +77,23 @@ public final class MockMvcBuilders {
|
||||
return new StandaloneMockMvcBuilder(controllers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a {@link MockMvc} instance by registering one or more
|
||||
* {@link RouterFunction RouterFunction} instances and configuring Spring
|
||||
* MVC infrastructure programmatically.
|
||||
* <p>This allows full control over the instantiation and initialization of
|
||||
* router functions and their dependencies, similar to plain unit tests while
|
||||
* also making it possible to test one router function at a time.
|
||||
* <p>When this builder is used, the minimum infrastructure required by the
|
||||
* {@link org.springframework.web.servlet.DispatcherServlet DispatcherServlet}
|
||||
* to serve requests with router functions is created automatically
|
||||
* and can be customized, resulting in configuration that is equivalent to
|
||||
* what MVC Java configuration provides except using builder-style methods.
|
||||
* @param routerFunctions one or more {@code RouterFunction} instances to test
|
||||
* @since 6.2
|
||||
*/
|
||||
public static RouterFunctionMockMvcBuilder routerFunctions(RouterFunction<?>... routerFunctions) {
|
||||
return new RouterFunctionMockMvcBuilder(routerFunctions);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@@ -596,23 +595,4 @@ public class StandaloneMockMvcBuilder extends AbstractMockMvcBuilder<StandaloneM
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A {@link ViewResolver} that always returns same View.
|
||||
*/
|
||||
private static 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,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 @@
|
||||
/*
|
||||
* 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 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,7 +125,7 @@ public class RouterFunctionMapping extends AbstractHandlerMapping implements Ini
|
||||
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
public void afterPropertiesSet() {
|
||||
if (this.routerFunction == null) {
|
||||
initRouterFunctions();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user