From 8b3bea173c3729fa3d56dcd76e6e78af89b17f53 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 3 Aug 2021 09:43:44 +0200 Subject: [PATCH] Configure WebTestClient for @AutoConfigureMockMvc tests As of Spring Framework 5.3, `WebTestClient` can now be configured on top of `MockMvc` for testing Spring MVC applications in a mock environment. Prior to this commit, `WebTestClient` would be already configured for WebFlux mock setups with `@AutoConfigureWebTestClient` or live servers (for both MVC and WebFlux apps). This commit enhances the `@AutoConfigureWebMvc` support so that a `WebTestClient` instance is auto-configured if the spring-webflux dependency is present on the classpath. Closes gh-23067 --- .../src/docs/asciidoc/features/testing.adoc | 9 ++-- .../withmockenvironment/MyMockMvcTests.java | 15 ++++++- .../web/servlet/MockMvcAutoConfiguration.java | 27 +++++++++++- .../main/resources/META-INF/spring.factories | 3 +- .../MockMvcAutoConfigurationTests.java | 43 ++++++++++++++++++- ...MockMvcSpringBootTestIntegrationTests.java | 8 +++- 6 files changed, 96 insertions(+), 9 deletions(-) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc index e8c0a63149..489da5cf22 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc @@ -172,8 +172,9 @@ include::{docs-java}/features/testing/springbootapplications/usingapplicationarg [[features.testing.spring-boot-applications.with-mock-environment]] ==== Testing with a mock environment -By default, `@SpringBootTest` does not start the server. -If you have web endpoints that you want to test against this mock environment, you can additionally configure {spring-framework-docs}/testing.html#spring-mvc-test-framework[`MockMvc`] as shown in the following example: +By default, `@SpringBootTest` 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 {spring-framework-docs}/testing.html#spring-mvc-test-framework[`MockMvc`] or `WebTestClient`, as shown in the following example: [source,java,indent=0,subs="verbatim"] ---- @@ -182,7 +183,7 @@ include::{docs-java}/features/testing/springbootapplications/withmockenvironment TIP: If you want to focus only on the web layer and not start a complete `ApplicationContext`, consider <>. -Alternatively, you can configure a {spring-framework-docs}/testing.html#webtestclient-tests[`WebTestClient`] as shown in the following example: +With Spring WebFlux endpoints, you can use {spring-framework-docs}/testing.html#webtestclient-tests[`WebTestClient`] as shown in the following example: [source,java,indent=0,subs="verbatim"] ---- @@ -214,6 +215,8 @@ For convenience, tests that need to make REST calls to the started server can ad include::{docs-java}/features/testing/springbootapplications/withrunningserver/MyRandomPortWebTestClientTests.java[] ---- +TIP: `WebTestClient` can be used against both live servers and <>. + This setup requires `spring-webflux` on the classpath. If you can't or won't add webflux, Spring Boot also provides a `TestRestTemplate` facility: diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/withmockenvironment/MyMockMvcTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/withmockenvironment/MyMockMvcTests.java index b89b927673..b869d880f6 100644 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/withmockenvironment/MyMockMvcTests.java +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/withmockenvironment/MyMockMvcTests.java @@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.test.web.servlet.MockMvc; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -32,8 +33,20 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. class MyMockMvcTests { @Test - void exampleTest(@Autowired MockMvc mvc) throws Exception { + void testWithMockMvc(@Autowired MockMvc mvc) throws Exception { mvc.perform(get("/")).andExpect(status().isOk()).andExpect(content().string("Hello World")); } + // If Spring WebFlux is on the classpath, you can drive MVC tests with a WebTestClient + @Test + void testWithWebTestClient(@Autowired WebTestClient webClient) { + // @formatter:off + webClient + .get().uri("/") + .exchange() + .expectStatus().isOk() + .expectBody(String.class).isEqualTo("Hello World"); + // @formatter:on + } + } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/MockMvcAutoConfiguration.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/MockMvcAutoConfiguration.java index b02d466c25..9fe168bef8 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/MockMvcAutoConfiguration.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/MockMvcAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 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.boot.test.autoconfigure.web.servlet; import java.util.List; import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; @@ -28,14 +29,19 @@ import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguratio import org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.autoconfigure.web.reactive.WebTestClientAutoConfiguration; +import org.springframework.boot.test.web.reactive.server.WebTestClientBuilderCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.test.web.servlet.DispatcherServletCustomizer; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvcBuilder; +import org.springframework.test.web.servlet.client.MockMvcWebTestClient; import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.servlet.DispatcherServlet; /** @@ -44,12 +50,13 @@ import org.springframework.web.servlet.DispatcherServlet; * @author Phillip Webb * @author Andy Wilkinson * @author Stephane Nicoll + * @author Brian Clozel * @see AutoConfigureWebMvc * @since 1.4.0 */ @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.SERVLET) -@AutoConfigureAfter(WebMvcAutoConfiguration.class) +@AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebTestClientAutoConfiguration.class }) @EnableConfigurationProperties({ ServerProperties.class, WebMvcProperties.class }) public class MockMvcAutoConfiguration { @@ -97,6 +104,22 @@ public class MockMvcAutoConfiguration { return mockMvc.getDispatcherServlet(); } + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass({ WebClient.class, WebTestClient.class }) + static class WebTestClientMockMvcConfiguration { + + @Bean + @ConditionalOnMissingBean + WebTestClient webTestClient(MockMvc mockMvc, List customizers) { + WebTestClient.Builder builder = MockMvcWebTestClient.bindTo(mockMvc); + for (WebTestClientBuilderCustomizer customizer : customizers) { + customizer.customize(builder); + } + return builder.build(); + } + + } + private static class MockMvcDispatcherServletCustomizer implements DispatcherServletCustomizer { private final WebMvcProperties webMvcProperties; diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring.factories index be8d3fb3ac..c0a4874dbd 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring.factories @@ -143,7 +143,8 @@ org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2Re org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration,\ org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration,\ org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration,\ -org.springframework.boot.test.autoconfigure.web.servlet.MockMvcSecurityConfiguration +org.springframework.boot.test.autoconfigure.web.servlet.MockMvcSecurityConfiguration,\ +org.springframework.boot.test.autoconfigure.web.reactive.WebTestClientAutoConfiguration # AutoConfigureMockRestServiceServer org.springframework.boot.test.autoconfigure.web.client.AutoConfigureMockRestServiceServer=\ diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/MockMvcAutoConfigurationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/MockMvcAutoConfigurationTests.java index 2af669ae2a..e7371fdd90 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/MockMvcAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/MockMvcAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 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,16 +19,26 @@ package org.springframework.boot.test.autoconfigure.web.servlet; 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.test.web.reactive.server.WebTestClientBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.servlet.DispatcherServlet; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; /** * Tests for {@link MockMvcAutoConfiguration}. * * @author Madhura Bhave + * @author Brian Clozel */ class MockMvcAutoConfigurationTests { @@ -44,4 +54,35 @@ class MockMvcAutoConfigurationTests { }); } + @Test + void registersWebTestClient() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(WebTestClient.class)); + } + + @Test + void shouldNotRegisterWebTestClientIfWebFluxMissing() { + this.contextRunner.withClassLoader(new FilteredClassLoader(WebClient.class)) + .run((context) -> assertThat(context).doesNotHaveBean(WebTestClient.class)); + } + + @Test + void shouldApplyWebTestClientCustomizers() { + this.contextRunner.withUserConfiguration(WebTestClientCustomConfig.class).run((context) -> { + assertThat(context).hasSingleBean(WebTestClient.class); + assertThat(context).hasBean("myWebTestClientCustomizer"); + verify(context.getBean("myWebTestClientCustomizer", WebTestClientBuilderCustomizer.class)) + .customize(any(WebTestClient.Builder.class)); + }); + } + + @Configuration(proxyBeanMethods = false) + static class WebTestClientCustomConfig { + + @Bean + WebTestClientBuilderCustomizer myWebTestClientCustomizer() { + return mock(WebTestClientBuilderCustomizer.class); + } + + } + } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/MockMvcSpringBootTestIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/MockMvcSpringBootTestIntegrationTests.java index 7304805b19..1ac7af7140 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/MockMvcSpringBootTestIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/MockMvcSpringBootTestIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 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. @@ -28,6 +28,7 @@ import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.context.ApplicationContext; import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.test.web.servlet.MockMvc; import static org.assertj.core.api.Assertions.assertThat; @@ -77,4 +78,9 @@ class MockMvcSpringBootTestIntegrationTests { assertThat(this.applicationContext.getBean(ExampleRealService.class)).isNotNull(); } + @Test + void shouldTestWithWebTestClient(@Autowired WebTestClient webTestClient) { + webTestClient.get().uri("/one").exchange().expectStatus().isOk().expectBody(String.class).isEqualTo("one"); + } + }