diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/spring-boot-applications.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/spring-boot-applications.adoc index 443d572b8d3..676ae57b7af 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/spring-boot-applications.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/spring-boot-applications.adoc @@ -141,10 +141,11 @@ include-code::MyApplicationArgumentTests[] By default, javadoc:org.springframework.boot.test.context.SpringBootTest[format=annotation] does not start the server but instead sets up a mock environment for testing web endpoints. With Spring MVC, we can query our web endpoints using {url-spring-framework-docs}/testing/mockmvc.html[`MockMvc`]. -Three integrations are available: +The following integrations are available: * The regular {url-spring-framework-docs}/testing/mockmvc/hamcrest.html[`MockMvc`] that uses Hamcrest. * {url-spring-framework-docs}/testing/mockmvc/assertj.html[`MockMvcTester`] that wraps javadoc:org.springframework.test.web.servlet.MockMvc[] and uses AssertJ. +* {url-spring-framework-docs}/testing/resttestclient.html[`RestTestClient`] where javadoc:org.springframework.test.web.servlet.MockMvc[] is plugged in as the server to handle requests with. * {url-spring-framework-docs}/testing/webtestclient.html[`WebTestClient`] where javadoc:org.springframework.test.web.servlet.MockMvc[] is plugged in as the server to handle requests with. The following example showcases the available integrations: @@ -176,19 +177,32 @@ If you need to start a full running server, we recommend that you use random por If you use `@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT)`, an available port is picked at random each time your test runs. The javadoc:org.springframework.boot.test.web.server.LocalServerPort[format=annotation] annotation can be used to xref:how-to:webserver.adoc#howto.webserver.discover-port[inject the actual port used] into your test. -For convenience, tests that need to make REST calls to the started server can additionally autowire a {url-spring-framework-docs}/testing/webtestclient.html[`WebTestClient`], which resolves relative links to the running server and comes with a dedicated API for verifying responses, as shown in the following example: + +For convenience, tests that need to make REST calls to the started server can additionally autowire a +{url-spring-framework-docs}/testing/resttestclient.html[`RestTestClient`] which resolves relative links to the running server and comes with a dedicated API for verifying responses, as shown in the following example: + +include-code::MyRandomPortRestTestClientTests[] + +If you have `spring-webflux` on the classpath, you can also autowire a {url-spring-framework-docs}/testing/webtestclient.html[`WebTestClient`] that provides a similar API: include-code::MyRandomPortWebTestClientTests[] TIP: javadoc:org.springframework.test.web.reactive.server.WebTestClient[] can also used with a xref:testing/spring-boot-applications.adoc#testing.spring-boot-applications.with-mock-environment[mock environment], removing the need for a running server, by annotating your test class with javadoc:org.springframework.boot.webflux.test.autoconfigure.AutoConfigureWebTestClient[format=annotation] from `spring-boot-webflux-test`. -This setup requires `spring-webflux` on the classpath. -If you can not or will not add webflux, the `spring-boot-web-server-test` modules provides a javadoc:org.springframework.boot.web.server.test.client.TestRestTemplate[] facility: +The `spring-boot-web-server-test` modules also provides a javadoc:org.springframework.boot.web.server.test.client.TestRestTemplate[] facility: include-code::MyRandomPortTestRestTemplateTests[] +[[testing.spring-boot-applications.customizing-rest-test-client]] +== Customizing RestTestClient + +To customize the javadoc:org.springframework.test.web.servlet.client.RestTestClient[] bean, configure a javadoc:org.springframework.boot.web.server.test.client.RestTestClientBuilderCustomizer[] bean. +Any such beans are called with the javadoc:org.springframework.test.web.servlet.client.RestTestClient$Builder[] that is used to create the javadoc:org.springframework.test.web.servlet.client.RestTestClient[]. + + + [[testing.spring-boot-applications.customizing-web-test-client]] == Customizing WebTestClient diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/test-utilities.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/test-utilities.adoc index 7965a8d6d7a..ab6e23c2a7d 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/test-utilities.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/test-utilities.adoc @@ -48,8 +48,9 @@ In either case, the template is fault tolerant. This means that it behaves in a test-friendly way by not throwing exceptions on 4xx and 5xx errors. Instead, such errors can be detected through the returned javadoc:org.springframework.http.ResponseEntity[] and its status code. -TIP: Spring Framework 5.0 provides a new javadoc:org.springframework.test.web.reactive.server.WebTestClient[] that works for xref:testing/spring-boot-applications.adoc#testing.spring-boot-applications.spring-webflux-tests[WebFlux integration tests] and both xref:testing/spring-boot-applications.adoc#testing.spring-boot-applications.with-running-server[WebFlux and MVC end-to-end testing]. -It provides a fluent API for assertions, unlike javadoc:org.springframework.boot.test.web.client.TestRestTemplate[]. +If you need fluent API for assertions, consider using javadoc:org.springframework.test.web.servlet.client.RestTestClient[] that works with xref:testing/spring-boot-applications.adoc#testing.spring-boot-applications.with-mock-environment[mock environments] and xref:testing/spring-boot-applications.adoc#testing.spring-boot-applications.with-running-server[end-to-end tests]. + +If you are using Spring WebFlux, consider the javadoc:org.springframework.test.web.reactive.server.WebTestClient[] that provides a similar API and works with xref:testing/spring-boot-applications.adoc#testing.spring-boot-applications.with-mock-environment[mock environments], xref:testing/spring-boot-applications.adoc#testing.spring-boot-applications.spring-webflux-tests[WebFlux integration tests], and xref:testing/spring-boot-applications.adoc#testing.spring-boot-applications.with-running-server[end-to-end tests]. It is recommended, but not mandatory, to use the Apache HTTP Client (version 5.1 or better). If you have that on your classpath, the javadoc:org.springframework.boot.test.web.client.TestRestTemplate[] responds by configuring the client appropriately. diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/springbootapplications/withmockenvironment/MyMockMvcTests.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/springbootapplications/withmockenvironment/MyMockMvcTests.java index 80dfa4785c6..1bf58b9d808 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/springbootapplications/withmockenvironment/MyMockMvcTests.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/springbootapplications/withmockenvironment/MyMockMvcTests.java @@ -24,6 +24,7 @@ import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.assertj.MockMvcTester; +import org.springframework.test.web.servlet.client.RestTestClient; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -45,6 +46,17 @@ class MyMockMvcTests { assertThat(mvc.get().uri("/")).hasStatusOk().hasBodyTextEqualTo("Hello World"); } + @Test + void testWithRestTestClient(@Autowired RestTestClient webClient) { + // @formatter:off + webClient + .get().uri("/") + .exchange() + .expectStatus().isOk() + .expectBody(String.class).isEqualTo("Hello World"); + // @formatter:on + } + // If Spring WebFlux is on the classpath, you can drive MVC tests with a WebTestClient @Test void testWithWebTestClient(@Autowired WebTestClient webClient) { diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/springbootapplications/withrunningserver/MyRandomPortRestTestClientTests.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/springbootapplications/withrunningserver/MyRandomPortRestTestClientTests.java new file mode 100644 index 00000000000..d551814b9ae --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/testing/springbootapplications/withrunningserver/MyRandomPortRestTestClientTests.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-present 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.boot.docs.testing.springbootapplications.withrunningserver; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.web.servlet.client.RestTestClient; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +class MyRandomPortRestTestClientTests { + + @Test + void exampleTest(@Autowired RestTestClient webClient) { + // @formatter:off + webClient + .get().uri("/") + .exchange() + .expectStatus().isOk() + .expectBody(String.class).isEqualTo("Hello World"); + // @formatter:on + } + +} diff --git a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RootUriBuilderFactory.java b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RootUriBuilderFactory.java index 57611cde279..1a65e2bea39 100644 --- a/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RootUriBuilderFactory.java +++ b/module/spring-boot-restclient/src/main/java/org/springframework/boot/restclient/RootUriBuilderFactory.java @@ -31,7 +31,12 @@ import org.springframework.web.util.UriTemplateHandler; */ public class RootUriBuilderFactory extends RootUriTemplateHandler implements UriBuilderFactory { - RootUriBuilderFactory(String rootUri, UriTemplateHandler delegate) { + /** + * Create an instance with the root URI to use. + * @param rootUri the root URI + * @param delegate the {@link UriTemplateHandler} to delegate to + */ + public RootUriBuilderFactory(String rootUri, UriTemplateHandler delegate) { super(rootUri, delegate); } diff --git a/module/spring-boot-web-server-test/src/main/java/org/springframework/boot/web/server/test/client/RestTestClientBuilderCustomizer.java b/module/spring-boot-web-server-test/src/main/java/org/springframework/boot/web/server/test/client/RestTestClientBuilderCustomizer.java new file mode 100644 index 00000000000..aa178a78c9a --- /dev/null +++ b/module/spring-boot-web-server-test/src/main/java/org/springframework/boot/web/server/test/client/RestTestClientBuilderCustomizer.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-present 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.boot.web.server.test.client; + +import org.springframework.test.web.servlet.client.RestTestClient; + +/** + * A customizer that can be implemented by beans wishing to customize the + * {@link RestTestClient.Builder} to fine-tine its auto-configuration before a + * {@link RestTestClient} is created. + * + * @author Stephane Nicoll + * @since 4.0.0 + */ +@FunctionalInterface +public interface RestTestClientBuilderCustomizer { + + /** + * Customize the given {@link RestTestClient.Builder Builder}. + * @param builder the builder + */ + void customize(RestTestClient.Builder builder); + +} diff --git a/module/spring-boot-web-server-test/src/main/java/org/springframework/boot/web/server/test/client/RestTestClientContextCustomizer.java b/module/spring-boot-web-server-test/src/main/java/org/springframework/boot/web/server/test/client/RestTestClientContextCustomizer.java new file mode 100644 index 00000000000..b8d6594860d --- /dev/null +++ b/module/spring-boot-web-server-test/src/main/java/org/springframework/boot/web/server/test/client/RestTestClientContextCustomizer.java @@ -0,0 +1,194 @@ +/* + * Copyright 2012-present 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.boot.web.server.test.client; + +import org.jspecify.annotations.Nullable; + +import org.springframework.aot.AotDetector; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.boot.restclient.RootUriBuilderFactory; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.server.reactive.AbstractReactiveWebServerFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.ConfigurationClassPostProcessor; +import org.springframework.core.Ordered; +import org.springframework.test.context.ContextCustomizer; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.test.context.TestContextAnnotationUtils; +import org.springframework.test.web.servlet.client.RestTestClient; +import org.springframework.util.Assert; + +/** + * {@link ContextCustomizer} for {@link RestTestClient}. + * + * @author Stephane Nicoll + */ +class RestTestClientContextCustomizer implements ContextCustomizer { + + @Override + public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) { + if (AotDetector.useGeneratedArtifacts()) { + return; + } + SpringBootTest springBootTest = TestContextAnnotationUtils.findMergedAnnotation(mergedConfig.getTestClass(), + SpringBootTest.class); + Assert.state(springBootTest != null, "'springBootTest' must not be null"); + if (springBootTest.webEnvironment().isEmbedded()) { + registerRestTestClient(context); + } + } + + private void registerRestTestClient(ConfigurableApplicationContext context) { + ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); + if (beanFactory instanceof BeanDefinitionRegistry registry) { + registerRestTestClient(registry); + } + } + + private void registerRestTestClient(BeanDefinitionRegistry registry) { + RootBeanDefinition definition = new RootBeanDefinition(RestTestClientRegistrar.class); + definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + registry.registerBeanDefinition(RestTestClientRegistrar.class.getName(), definition); + } + + @Override + public boolean equals(@Nullable Object obj) { + return (obj != null) && (obj.getClass() == getClass()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + + /** + * {@link BeanDefinitionRegistryPostProcessor} that runs after the + * {@link ConfigurationClassPostProcessor} and add a {@link RestTestClientFactory} + * bean definition when a {@link RestTestClient} hasn't already been registered. + */ + static class RestTestClientRegistrar implements BeanDefinitionRegistryPostProcessor, Ordered, BeanFactoryAware { + + @SuppressWarnings("NullAway.Init") + private BeanFactory beanFactory; + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } + + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { + if (AotDetector.useGeneratedArtifacts()) { + return; + } + if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors((ListableBeanFactory) this.beanFactory, + RestTestClient.class, false, false).length == 0) { + registry.registerBeanDefinition(RestTestClient.class.getName(), + new RootBeanDefinition(RestTestClientFactory.class)); + } + + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + } + + } + + /** + * {@link FactoryBean} used to create and configure a {@link RestTestClient}. + */ + public static class RestTestClientFactory implements FactoryBean, ApplicationContextAware { + + @SuppressWarnings("NullAway.Init") + private ApplicationContext applicationContext; + + private @Nullable RestTestClient object; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + @Override + public boolean isSingleton() { + return true; + } + + @Override + public Class getObjectType() { + return RestTestClient.class; + } + + @Override + public RestTestClient getObject() { + if (this.object == null) { + this.object = createRestTestClient(); + } + return this.object; + } + + private RestTestClient createRestTestClient() { + boolean sslEnabled = isSslEnabled(this.applicationContext); + LocalHostUriTemplateHandler handler = new LocalHostUriTemplateHandler( + this.applicationContext.getEnvironment(), sslEnabled ? "https" : "http"); + RestTestClient.Builder builder = RestTestClient.bindToServer(); + customizeRestTestClientBuilder(builder, this.applicationContext); + return builder.uriBuilderFactory(new RootUriBuilderFactory(handler.getRootUri(), handler)).build(); + } + + private boolean isSslEnabled(ApplicationContext context) { + try { + AbstractReactiveWebServerFactory webServerFactory = context + .getBean(AbstractReactiveWebServerFactory.class); + return webServerFactory.getSsl() != null && webServerFactory.getSsl().isEnabled(); + } + catch (NoSuchBeanDefinitionException ex) { + return false; + } + } + + private void customizeRestTestClientBuilder(RestTestClient.Builder clientBuilder, + ApplicationContext context) { + for (RestTestClientBuilderCustomizer customizer : context + .getBeansOfType(RestTestClientBuilderCustomizer.class) + .values()) { + customizer.customize(clientBuilder); + } + } + + } + +} diff --git a/module/spring-boot-web-server-test/src/main/java/org/springframework/boot/web/server/test/client/RestTestClientContextCustomizerFactory.java b/module/spring-boot-web-server-test/src/main/java/org/springframework/boot/web/server/test/client/RestTestClientContextCustomizerFactory.java new file mode 100644 index 00000000000..7b4a11bd263 --- /dev/null +++ b/module/spring-boot-web-server-test/src/main/java/org/springframework/boot/web/server/test/client/RestTestClientContextCustomizerFactory.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-present 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.boot.web.server.test.client; + +import java.util.List; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ContextConfigurationAttributes; +import org.springframework.test.context.ContextCustomizer; +import org.springframework.test.context.ContextCustomizerFactory; +import org.springframework.test.context.TestContextAnnotationUtils; +import org.springframework.util.ClassUtils; + +/** + * {@link ContextCustomizerFactory} for {@code RestTestClient}. + * + * @author Stephane Nicoll + */ +class RestTestClientContextCustomizerFactory implements ContextCustomizerFactory { + + private static final boolean restClientPresent; + + static { + ClassLoader loader = RestTestClientContextCustomizerFactory.class.getClassLoader(); + restClientPresent = ClassUtils.isPresent("org.springframework.web.client.RestClient", loader); + } + + @Override + public @Nullable ContextCustomizer createContextCustomizer(Class testClass, + List configAttributes) { + SpringBootTest springBootTest = TestContextAnnotationUtils.findMergedAnnotation(testClass, + SpringBootTest.class); + return (springBootTest != null && restClientPresent) ? new RestTestClientContextCustomizer() : null; + } + +} diff --git a/module/spring-boot-web-server-test/src/main/resources/META-INF/spring.factories b/module/spring-boot-web-server-test/src/main/resources/META-INF/spring.factories index ee04e5c0d73..7b66b762df7 100644 --- a/module/spring-boot-web-server-test/src/main/resources/META-INF/spring.factories +++ b/module/spring-boot-web-server-test/src/main/resources/META-INF/spring.factories @@ -4,6 +4,7 @@ org.springframework.boot.web.server.test.SpringBootTestRandomPortEnvironmentPost # Spring Test Context Customizer Factories org.springframework.test.context.ContextCustomizerFactory=\ +org.springframework.boot.web.server.test.client.RestTestClientContextCustomizerFactory,\ org.springframework.boot.web.server.test.client.TestRestTemplateContextCustomizerFactory,\ org.springframework.boot.web.server.test.client.reactive.WebTestClientContextCustomizerFactory,\ org.springframework.boot.web.server.test.reactor.netty.DisableReactorResourceFactoryGlobalResourcesContextCustomizerFactory diff --git a/module/spring-boot-web-server-test/src/test/java/org/springframework/boot/web/server/test/client/NoRestTestClientBeanChecker.java b/module/spring-boot-web-server-test/src/test/java/org/springframework/boot/web/server/test/client/NoRestTestClientBeanChecker.java new file mode 100644 index 00000000000..6c446818810 --- /dev/null +++ b/module/spring-boot-web-server-test/src/test/java/org/springframework/boot/web/server/test/client/NoRestTestClientBeanChecker.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-present 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.boot.web.server.test.client; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.context.annotation.ImportSelector; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.test.web.servlet.client.RestTestClient; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * {@link ImportSelector} to check no {@link RestTestClient} definition is registered when + * config classes are processed. + */ +class NoRestTestClientBeanChecker implements ImportSelector, BeanFactoryAware { + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + assertThat(BeanFactoryUtils.beanNamesForTypeIncludingAncestors((ListableBeanFactory) beanFactory, + RestTestClient.class)) + .isEmpty(); + } + + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + return new String[0]; + } + +} diff --git a/module/spring-boot-web-server-test/src/test/java/org/springframework/boot/web/server/test/client/RestTestClientContextCustomizerIntegrationTests.java b/module/spring-boot-web-server-test/src/test/java/org/springframework/boot/web/server/test/client/RestTestClientContextCustomizerIntegrationTests.java new file mode 100644 index 00000000000..65ac84171ca --- /dev/null +++ b/module/spring-boot-web-server-test/src/test/java/org/springframework/boot/web/server/test/client/RestTestClientContextCustomizerIntegrationTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-present 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.boot.web.server.test.client; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.web.servlet.client.RestTestClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Integration test for {@link RestTestClientContextCustomizer}. + * + * @author Stephane Nicoll + */ +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@DirtiesContext +class RestTestClientContextCustomizerIntegrationTests { + + @Autowired + private RestTestClient webClient; + + @Autowired + private RestTestClientBuilderCustomizer clientBuilderCustomizer; + + @Test + void test() { + then(this.clientBuilderCustomizer).should().customize(any(RestTestClient.Builder.class)); + this.webClient.get().uri("/").exchange().expectBody(String.class).isEqualTo("hello"); + } + + @Configuration(proxyBeanMethods = false) + @Import({ TestWebMvcConfiguration.class, NoRestTestClientBeanChecker.class }) + @RestController + static class TestConfig { + + @GetMapping("/") + String root() { + return "hello"; + } + + @Bean + RestTestClientBuilderCustomizer clientBuilderCustomizer() { + return mock(RestTestClientBuilderCustomizer.class); + } + + } + +} diff --git a/module/spring-boot-web-server-test/src/test/java/org/springframework/boot/web/server/test/client/RestTestClientContextCustomizerTests.java b/module/spring-boot-web-server-test/src/test/java/org/springframework/boot/web/server/test/client/RestTestClientContextCustomizerTests.java new file mode 100644 index 00000000000..2061c80f712 --- /dev/null +++ b/module/spring-boot-web-server-test/src/test/java/org/springframework/boot/web/server/test/client/RestTestClientContextCustomizerTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012-present 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.boot.web.server.test.client; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.web.server.test.client.RestTestClientContextCustomizer.RestTestClientRegistrar; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.AbstractApplicationContext; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.test.web.servlet.client.RestTestClient; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link RestTestClientContextCustomizer}. + * + * @author Moritz Halbritter + */ +class RestTestClientContextCustomizerTests { + + @Test + void whenContextIsNotABeanDefinitionRegistryRestTestClientIsRegistered() { + new ApplicationContextRunner(TestApplicationContext::new) + .withInitializer(this::applyRestTestClientContextCustomizer) + .run((context) -> assertThat(context).hasSingleBean(RestTestClient.class)); + } + + @Test + void whenUsingAotGeneratedArtifactsRestTestClientIsNotRegistered() { + new ApplicationContextRunner().withSystemProperties("spring.aot.enabled:true") + .withInitializer(this::applyRestTestClientContextCustomizer) + .run((context) -> { + assertThat(context).doesNotHaveBean(RestTestClientRegistrar.class); + assertThat(context).doesNotHaveBean(RestTestClient.class); + }); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + void applyRestTestClientContextCustomizer(ConfigurableApplicationContext context) { + MergedContextConfiguration configuration = mock(MergedContextConfiguration.class); + given(configuration.getTestClass()).willReturn((Class) TestClass.class); + new RestTestClientContextCustomizer().customizeContext(context, configuration); + } + + @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) + static class TestClass { + + } + + static class TestApplicationContext extends AbstractApplicationContext { + + private final ConfigurableListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + + @Override + protected void refreshBeanFactory() { + } + + @Override + protected void closeBeanFactory() { + + } + + @Override + public ConfigurableListableBeanFactory getBeanFactory() { + return this.beanFactory; + } + + } + +} diff --git a/module/spring-boot-web-server-test/src/test/java/org/springframework/boot/web/server/test/client/RestTestClientContextCustomizerWithCustomContextPathIntegrationTests.java b/module/spring-boot-web-server-test/src/test/java/org/springframework/boot/web/server/test/client/RestTestClientContextCustomizerWithCustomContextPathIntegrationTests.java new file mode 100644 index 00000000000..92af883e422 --- /dev/null +++ b/module/spring-boot-web-server-test/src/test/java/org/springframework/boot/web/server/test/client/RestTestClientContextCustomizerWithCustomContextPathIntegrationTests.java @@ -0,0 +1,88 @@ +/* + * Copyright 2012-present 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.boot.web.server.test.client; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory; +import org.springframework.boot.web.server.servlet.ServletWebServerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.client.RestTestClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.DispatcherServlet; + +/** + * Integration test for {@link RestTestClientContextCustomizer} with a custom context + * path. + * + * @author Stephane Nicoll + */ +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@TestPropertySource(properties = "server.servlet.context-path=/test") +class RestTestClientContextCustomizerWithCustomContextPathIntegrationTests { + + @Autowired + private RestTestClient webClient; + + @Test + void test() { + this.webClient.get().uri("/").exchange().expectBody(String.class).isEqualTo("hello"); + } + + @Configuration(proxyBeanMethods = false) + @Import(NoRestTestClientBeanChecker.class) + @RestController + static class TestConfig { + + @Value("${server.port:8080}") + private int port = 8080; + + @Bean + DispatcherServlet dispatcherServlet() { + return new DispatcherServlet(); + } + + @Bean + ServletWebServerFactory webServerFactory() { + TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(); + factory.setPort(this.port); + factory.setContextPath("/test"); + return factory; + } + + @Bean + static PropertySourcesPlaceholderConfigurer propertyPlaceholder() { + return new PropertySourcesPlaceholderConfigurer(); + } + + @GetMapping("/") + String root() { + return "hello"; + } + + } + +} diff --git a/module/spring-boot-web-server-test/src/test/java/org/springframework/boot/web/server/test/client/RestTestClientContextCustomizerWithOverridePathIntegrationTests.java b/module/spring-boot-web-server-test/src/test/java/org/springframework/boot/web/server/test/client/RestTestClientContextCustomizerWithOverridePathIntegrationTests.java new file mode 100644 index 00000000000..cde1457c7b1 --- /dev/null +++ b/module/spring-boot-web-server-test/src/test/java/org/springframework/boot/web/server/test/client/RestTestClientContextCustomizerWithOverridePathIntegrationTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-present 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.boot.web.server.test.client; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.test.web.servlet.client.RestTestClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Integration test for {@link RestTestClientContextCustomizer} with a custom client. + * + * @author Stephane Nicoll + */ +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +class RestTestClientContextCustomizerWithOverridePathIntegrationTests { + + @Autowired + private RestTestClient webClient; + + @Test + void test() { + assertThat(this.webClient).isInstanceOf(CustomRestTestClient.class); + } + + @Configuration(proxyBeanMethods = false) + @Import({ TestWebMvcConfiguration.class, NoRestTestClientBeanChecker.class }) + @RestController + static class TestConfig { + + @GetMapping("/") + String root() { + return "hello"; + } + + @Bean + CustomRestTestClient customRestTestClient() { + return mock(CustomRestTestClient.class); + } + + } + + interface CustomRestTestClient extends RestTestClient { + + } + +} diff --git a/module/spring-boot-web-server-test/src/test/java/org/springframework/boot/web/server/test/client/TestWebMvcConfiguration.java b/module/spring-boot-web-server-test/src/test/java/org/springframework/boot/web/server/test/client/TestWebMvcConfiguration.java new file mode 100644 index 00000000000..e6f22b4186c --- /dev/null +++ b/module/spring-boot-web-server-test/src/test/java/org/springframework/boot/web/server/test/client/TestWebMvcConfiguration.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-present 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.boot.web.server.test.client; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory; +import org.springframework.boot.web.server.servlet.ServletWebServerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.web.servlet.DispatcherServlet; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +@Configuration(proxyBeanMethods = false) +@EnableWebMvc +class TestWebMvcConfiguration { + + @Value("${server.port:8080}") + private int port = 8080; + + @Bean + DispatcherServlet dispatcherServlet() { + return new DispatcherServlet(); + } + + @Bean + ServletWebServerFactory webServerFactory() { + TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(); + factory.setPort(this.port); + return factory; + } + + @Bean + static PropertySourcesPlaceholderConfigurer propertyPlaceholder() { + return new PropertySourcesPlaceholderConfigurer(); + } + +} diff --git a/module/spring-boot-webmvc-test/src/main/java/org/springframework/boot/webmvc/test/autoconfigure/MockMvcAutoConfiguration.java b/module/spring-boot-webmvc-test/src/main/java/org/springframework/boot/webmvc/test/autoconfigure/MockMvcAutoConfiguration.java index b56b317a730..d3cef4a06d1 100644 --- a/module/spring-boot-webmvc-test/src/main/java/org/springframework/boot/webmvc/test/autoconfigure/MockMvcAutoConfiguration.java +++ b/module/spring-boot-webmvc-test/src/main/java/org/springframework/boot/webmvc/test/autoconfigure/MockMvcAutoConfiguration.java @@ -25,6 +25,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplicat import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.web.server.autoconfigure.ServerProperties; +import org.springframework.boot.web.server.test.client.RestTestClientBuilderCustomizer; import org.springframework.boot.web.server.test.client.reactive.WebTestClientBuilderCustomizer; import org.springframework.boot.webmvc.autoconfigure.DispatcherServletPath; import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration; @@ -35,6 +36,8 @@ import org.springframework.context.annotation.Import; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.client.MockMvcWebTestClient; +import org.springframework.test.web.servlet.client.RestTestClient; +import org.springframework.web.client.RestClient; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.servlet.DispatcherServlet; @@ -82,4 +85,20 @@ public final class MockMvcAutoConfiguration { } + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass({ RestClient.class, RestTestClient.class }) + static class RestTestClientMockMvcConfiguration { + + @Bean + @ConditionalOnMissingBean + RestTestClient restTestClient(MockMvc mockMvc, List customizers) { + RestTestClient.Builder builder = RestTestClient.bindTo(mockMvc); + for (RestTestClientBuilderCustomizer customizer : customizers) { + customizer.customize(builder); + } + return builder.build(); + } + + } + } diff --git a/module/spring-boot-webmvc-test/src/test/java/org/springframework/boot/webmvc/test/autoconfigure/MockMvcAutoConfigurationTests.java b/module/spring-boot-webmvc-test/src/test/java/org/springframework/boot/webmvc/test/autoconfigure/MockMvcAutoConfigurationTests.java index 0420234a1f6..85e4f80c112 100644 --- a/module/spring-boot-webmvc-test/src/test/java/org/springframework/boot/webmvc/test/autoconfigure/MockMvcAutoConfigurationTests.java +++ b/module/spring-boot-webmvc-test/src/test/java/org/springframework/boot/webmvc/test/autoconfigure/MockMvcAutoConfigurationTests.java @@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.web.server.test.client.RestTestClientBuilderCustomizer; import org.springframework.boot.web.server.test.client.reactive.WebTestClientBuilderCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -28,6 +29,8 @@ import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.RequestBuilder; import org.springframework.test.web.servlet.assertj.MockMvcTester; +import org.springframework.test.web.servlet.client.RestTestClient; +import org.springframework.web.client.RestClient; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.servlet.DispatcherServlet; @@ -41,6 +44,7 @@ import static org.mockito.Mockito.mock; * * @author Madhura Bhave * @author Brian Clozel + * @author Stephane Nicoll */ class MockMvcAutoConfigurationTests { @@ -99,6 +103,27 @@ class MockMvcAutoConfigurationTests { }); } + @Test + void registersRestTestClient() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(RestTestClient.class)); + } + + @Test + void shouldNotRegisterRestTestClientIfRestClientIsMissing() { + this.contextRunner.withClassLoader(new FilteredClassLoader(RestClient.class)) + .run((context) -> assertThat(context).doesNotHaveBean(RestTestClient.class)); + } + + @Test + void shouldApplyRestTestClientCustomizers() { + this.contextRunner.withUserConfiguration(RestTestClientCustomConfig.class).run((context) -> { + assertThat(context).hasSingleBean(RestTestClient.class); + assertThat(context).hasBean("myRestTestClientCustomizer"); + then(context.getBean("myRestTestClientCustomizer", RestTestClientBuilderCustomizer.class)).should() + .customize(any(RestTestClient.Builder.class)); + }); + } + @Configuration(proxyBeanMethods = false) static class WebTestClientCustomConfig { @@ -109,4 +134,14 @@ class MockMvcAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class RestTestClientCustomConfig { + + @Bean + RestTestClientBuilderCustomizer myRestTestClientCustomizer() { + return mock(RestTestClientBuilderCustomizer.class); + } + + } + } diff --git a/module/spring-boot-webmvc-test/src/test/java/org/springframework/boot/webmvc/test/autoconfigure/mockmvc/MockMvcSpringBootTestIntegrationTests.java b/module/spring-boot-webmvc-test/src/test/java/org/springframework/boot/webmvc/test/autoconfigure/mockmvc/MockMvcSpringBootTestIntegrationTests.java index f2f69a2f63c..2e0a6ded020 100644 --- a/module/spring-boot-webmvc-test/src/test/java/org/springframework/boot/webmvc/test/autoconfigure/mockmvc/MockMvcSpringBootTestIntegrationTests.java +++ b/module/spring-boot-webmvc-test/src/test/java/org/springframework/boot/webmvc/test/autoconfigure/mockmvc/MockMvcSpringBootTestIntegrationTests.java @@ -29,6 +29,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.client.RestTestClient; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -84,6 +85,11 @@ class MockMvcSpringBootTestIntegrationTests { webTestClient.get().uri("/one").exchange().expectStatus().isOk().expectBody(String.class).isEqualTo("one"); } + @Test + void shouldTestWithRestTestClient(@Autowired RestTestClient restTestClient) { + restTestClient.get().uri("/one").exchange().expectStatus().isOk().expectBody(String.class).isEqualTo("one"); + } + @Test void shouldNotFailIfFormattingValueThrowsException(CapturedOutput output) throws Exception { this.mvc.perform(get("/formatting")).andExpect(content().string("formatting")).andExpect(status().isOk()); diff --git a/module/spring-boot-webmvc-test/src/test/java/org/springframework/boot/webmvc/test/autoconfigure/mockmvc/MockMvcTesterSpringBootTestIntegrationTests.java b/module/spring-boot-webmvc-test/src/test/java/org/springframework/boot/webmvc/test/autoconfigure/mockmvc/MockMvcTesterSpringBootTestIntegrationTests.java index f3016eb0371..5fccc3bb0f4 100644 --- a/module/spring-boot-webmvc-test/src/test/java/org/springframework/boot/webmvc/test/autoconfigure/mockmvc/MockMvcTesterSpringBootTestIntegrationTests.java +++ b/module/spring-boot-webmvc-test/src/test/java/org/springframework/boot/webmvc/test/autoconfigure/mockmvc/MockMvcTesterSpringBootTestIntegrationTests.java @@ -29,6 +29,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.test.web.servlet.assertj.MockMvcTester; +import org.springframework.test.web.servlet.client.RestTestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -80,6 +81,11 @@ class MockMvcTesterSpringBootTestIntegrationTests { webTestClient.get().uri("/one").exchange().expectStatus().isOk().expectBody(String.class).isEqualTo("one"); } + @Test + void shouldTestWithRestTestClient(@Autowired RestTestClient restTestClient) { + restTestClient.get().uri("/one").exchange().expectStatus().isOk().expectBody(String.class).isEqualTo("one"); + } + @Test void shouldNotFailIfFormattingValueThrowsException(CapturedOutput output) { assertThat(this.mvc.get().uri("/formatting")).hasStatusOk().hasBodyTextEqualTo("formatting");