Browse Source

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
pull/32755/head
Arjen Poutsma 2 years ago committed by Arjen Poutsma
parent
commit
8f3b7484e2
  1. 86
      spring-test/src/main/java/org/springframework/test/web/servlet/client/MockMvcWebTestClient.java
  2. 100
      spring-test/src/main/java/org/springframework/test/web/servlet/client/RouterFunctionMockMvcSpec.java
  3. 22
      spring-test/src/main/java/org/springframework/test/web/servlet/setup/MockMvcBuilders.java
  4. 321
      spring-test/src/main/java/org/springframework/test/web/servlet/setup/RouterFunctionMockMvcBuilder.java
  5. 20
      spring-test/src/main/java/org/springframework/test/web/servlet/setup/StandaloneMockMvcBuilder.java
  6. 44
      spring-test/src/main/java/org/springframework/test/web/servlet/setup/StaticViewResolver.java
  7. 137
      spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/RouterFunctionTests.java
  8. 167
      spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/RouterFunctionTests.java
  9. 2
      spring-webmvc/src/main/java/org/springframework/web/servlet/function/support/RouterFunctionMapping.java

86
spring-test/src/main/java/org/springframework/test/web/servlet/client/MockMvcWebTestClient.java

@ -1,5 +1,5 @@ @@ -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; @@ -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; @@ -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 { @@ -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 { @@ -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);
}
}

100
spring-test/src/main/java/org/springframework/test/web/servlet/client/RouterFunctionMockMvcSpec.java

@ -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;
}
}

22
spring-test/src/main/java/org/springframework/test/web/servlet/setup/MockMvcBuilders.java

@ -1,5 +1,5 @@ @@ -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; @@ -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 { @@ -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);
}
}

321
spring-test/src/main/java/org/springframework/test/web/servlet/setup/RouterFunctionMockMvcBuilder.java

@ -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);
}
}
}
}

20
spring-test/src/main/java/org/springframework/test/web/servlet/setup/StandaloneMockMvcBuilder.java

@ -21,7 +21,6 @@ import java.util.Arrays; @@ -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 @@ -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;
}
}
}

44
spring-test/src/main/java/org/springframework/test/web/servlet/setup/StaticViewResolver.java

@ -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;
}
}

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

@ -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;
}
}
}

167
spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/RouterFunctionTests.java

@ -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;
}
}
}

2
spring-webmvc/src/main/java/org/springframework/web/servlet/function/support/RouterFunctionMapping.java

@ -125,7 +125,7 @@ public class RouterFunctionMapping extends AbstractHandlerMapping implements Ini @@ -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();
}

Loading…
Cancel
Save