在集成测试中覆盖 bean
Overriding beans in Integration tests
对于我的 Spring-Boot 应用程序,我通过 @Configuration 文件提供了一个 RestTemplate,因此我可以添加合理的默认值(例如超时)。对于我的集成测试,我想模拟 RestTemplate,因为我不想连接到外部服务——我知道期望的响应。我尝试在集成测试包中提供不同的实现,希望后者会覆盖真正的实现,但检查日志却是相反的:真正的实现覆盖了测试。
如何确定 TestConfig 中的那个是使用的那个?
这是我的配置文件:
@Configuration
public class RestTemplateProvider {
private static final int DEFAULT_SERVICE_TIMEOUT = 5_000;
@Bean
public RestTemplate restTemplate(){
return new RestTemplate(buildClientConfigurationFactory());
}
private ClientHttpRequestFactory buildClientConfigurationFactory() {
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setReadTimeout(DEFAULT_SERVICE_TIMEOUT);
factory.setConnectTimeout(DEFAULT_SERVICE_TIMEOUT);
return factory;
}
}
集成测试:
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = TestConfiguration.class)
@WebAppConfiguration
@ActiveProfiles("it")
public abstract class IntegrationTest {}
测试配置class:
@Configuration
@Import({Application.class, MockRestTemplateConfiguration.class})
public class TestConfiguration {}
最后是 MockRestTemplateConfiguration
@Configuration
public class MockRestTemplateConfiguration {
@Bean
public RestTemplate restTemplate() {
return Mockito.mock(RestTemplate.class)
}
}
1。
您可以使用 @Primary
注释:
@Configuration
public class MockRestTemplateConfiguration {
@Bean
@Primary
public RestTemplate restTemplate() {
return Mockito.mock(RestTemplate.class)
}
}
顺便说一句,我写了blog post about faking Spring bean
2。
但我建议看一下 Spring RestTemplate testing support。这将是一个简单的例子:
private MockRestServiceServer mockServer;
@Autowired
private RestTemplate restTemplate;
@Autowired
private UsersClient usersClient;
@BeforeMethod
public void init() {
mockServer = MockRestServiceServer.createServer(restTemplate);
}
@Test
public void testSingleGet() throws Exception {
// GIVEN
int testingIdentifier = 0;
mockServer.expect(requestTo(USERS_URL + "/" + testingIdentifier))
.andExpect(method(HttpMethod.GET))
.andRespond(withSuccess(TEST_RECORD0, MediaType.APPLICATION_JSON));
// WHEN
User user = usersClient.getUser(testingIdentifier);
// THEN
mockServer.verify();
assertEquals(user.getName(), USER0_NAME);
assertEquals(user.getEmail(), USER0_EMAIL);
}
中找到更多示例
自 Spring Boot 1.4.x 有一个选项可以使用 @MockBean
注释来伪造 Spring bean。
对评论的反应:
要在缓存中保留上下文,请不要使用 @DirtiesContext
,但使用 @ContextConfiguration(name = "contextWithFakeBean")
它将创建单独的上下文,同时它会将默认上下文保留在缓存中。 Spring 会将两者(或您有多少上下文)保存在缓存中。
我们的构建是这样的,其中大多数测试都使用默认的非污染配置,但我们有 4-5 个测试是伪造的 beans。默认上下文被很好地重用
再深入一点,看我的第二个回答。
我使用
解决了问题
@SpringBootTest(classes = {AppConfiguration.class, AppTestConfiguration.class})
而不是
@Import({ AppConfiguration.class, AppTestConfiguration.class });
在我的例子中,测试与应用程序不在同一个包中。所以我需要明确指定 AppConfiguration.class(或 App.class)。如果你在测试中使用相同的包,我想你可以只写
@SpringBootTest(classes = AppTestConfiguration.class)
而不是(不工作)
@Import(AppTestConfiguration.class );
看到这如此不同,真是太棒了。也许有人可以解释这一点。直到现在我找不到任何好的答案。您可能会认为,如果 @SpringBootTests
存在,则 @Import(...)
不会被拾取,但是在日志中显示了覆盖 bean。只是方向错了。
顺便说一下,使用 @TestConfiguration
代替 @Configuration
也没有区别。
您配置中的问题是您正在使用 @Configuration
作为您的测试配置。这将替换您的主要配置。而是使用 @TestConfiguration
这将附加(覆盖)您的主要配置。
46.3.2 Detecting Test Configuration
If you want to customize the primary configuration, you can use a
nested @TestConfiguration class. Unlike a nested @Configuration class,
which would be used instead of your application’s primary
configuration, a nested @TestConfiguration class is used in addition
to your application’s primary configuration.
使用 SpringBoot 的示例:
主要class
@SpringBootApplication() // Will scan for @Components and @Configs in package tree
public class Main{
}
主要配置
@Configuration
public void AppConfig() {
// Define any beans
}
测试配置
@TestConfiguration
public void AppTestConfig(){
// override beans for testing
}
测试class
@RunWith(SpringRunner.class)
@Import(AppTestConfig.class)
@SpringBootTest
public void AppTest() {
// use @MockBean if you like
}
注意:请注意,所有 Bean 都将被创建,即使是您覆盖的那些。如果您不想实例化 @Configuration
.
,请使用 @Profile
检查 答案以及该线程中提供的其他答案。
它是关于在 Spring Boot 2.X 中覆盖 bean,默认情况下此选项是禁用的。如果您决定走那条路,它还有一些关于如何使用 Bean 定义 DSL 的想法。
@MockBean
和 OP 使用的 bean 覆盖是两种互补的方法。
您想使用 @MockBean
创建模拟并忘记真正的实现:通常您这样做是为了切片测试或集成测试,它们不会加载某些 class(es) 的 bean您正在测试依赖并且 您不想在集成中测试这些 bean。
Spring 默认使它们成为 null
,您将模拟它们的最小行为以完成您的测试。
@WebMvcTest
经常需要该策略,因为您不想测试整个层,并且 @SpringBootTest
也可能需要,如果您在测试配置中仅指定 beans 配置的一个子集.
另一方面,有时您希望使用尽可能多的真实组件执行集成测试,因此您不想使用 @MockBean
但您希望稍微覆盖一个行为,一个依赖项或者为 bean 定义一个新的范围,在这种情况下,要遵循的方法是 bean 覆盖:
@SpringBootTest({"spring.main.allow-bean-definition-overriding=true"})
@Import(FooTest.OverrideBean.class)
public class FooTest{
@Test
public void getFoo() throws Exception {
// ...
}
@TestConfiguration
public static class OverrideBean {
// change the bean scope to SINGLETON
@Bean
@Scope(ConfigurableBeanFactory.SINGLETON)
public Bar bar() {
return new Bar();
}
// use a stub for a bean
@Bean
public FooBar BarFoo() {
return new BarFooStub();
}
// use a stub for the dependency of a bean
@Bean
public FooBar fooBar() {
return new FooBar(new StubDependency());
}
}
}
使用@Primary 注释,Bean 覆盖与 Spring Boot 1.5.X 一起工作,但在 Spring Boot 2.1.X 下失败,它抛出错误:
Invalid bean definition with name 'testBean' defined in sample..ConfigTest$SpringConfig:..
There is already .. defined in class path resource [TestConfig.class]] bound
请在下面添加 properties=
,这将明确指示 Spring 允许覆盖,这是不言自明的。
@SpringBootTest(properties = ["spring.main.allow-bean-definition-overriding=true"])
更新:您可以在 application-test.yml 中添加相同的 属性(文件名取决于您正在测试的测试配置文件名称)
我在测试中声明了一个内部配置 class 因为我只想覆盖一个方法
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class FileNotificationWebhookTest{
public static class FileNotificationWebhookTestConfiguration {
@Bean
@Primary
public FileJobRequestConverter fileJobRequestConverter() {
return new FileJobRequestConverter() {
@Override
protected File resolveWindowsPath(String path) {
return new File(path);
}
};
}
}
}
然而,
在 @SpringBootTest 中声明配置无效:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,classes = {FileNotificationWebhookTest.FileNotificationWebhookTestConfiguration.class})
或使用 @Configuration 注释测试配置无效:
@Configuration
public static class FileNotificationWebhookTestConfiguration {
}
并导致
Caused by: org.springframework.context.ApplicationContextException:
Unable to start web server; nested exception is
org.springframework.context.ApplicationContextException: Unable to
start ServletWebServerApplicationContext due to missing
ServletWebServerFactory bean.
对我有用的(与这里的其他一些帖子相反)是使用@Import
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Import(FileNotificationWebhookTest.FileNotificationWebhookTestConfiguration.class)
class FileNotificationWebhookTest {
}
使用 Spring:5.3.3 和 Spring-Boot-Starter:2.4.2
@MockBean
创建 Mockito 模拟而不是生产构建。
如果您不想使用 Mockito,但以其他方式提供替代品(即通过功能切换禁用 bean 的某些功能),我建议使用 @TestConfiguration
的组合(因为 Spring Boot 1.4.0) 和 @Primary
注释。
@TestConfiguration
将加载您的默认上下文并应用您的 @TestConfiguration
片段。添加 @Primary
将强制将模拟的 RestTemplate 注入到它的依赖项中。
请参阅下面的简化示例:
@SpringBootTest
public class ServiceTest {
@TestConfiguration
static class AdditionalCfg {
@Primary
@Bean
RestTemplate rt() {
return new RestTemplate() {
@Override
public String exec() {
return "Test rest template";
}
};
}
}
@Autowired
MyService myService;
@Test
void contextLoads() {
assertThat(myService.invoke()).isEqualTo("Test rest template");
}
}
我找到的最简单的解决方案是在 application.properties 中设置这个 属性:
spring.main.allow-bean-definition-overriding=true
这将启用 bean 的覆盖。
接下来,在测试中创建一个配置 class,并用以下内容注释您的 bean:
@Bean
@Primary
这样,当 运行 测试时,这个 bean 将覆盖您常用的 bean。
对于我的 Spring-Boot 应用程序,我通过 @Configuration 文件提供了一个 RestTemplate,因此我可以添加合理的默认值(例如超时)。对于我的集成测试,我想模拟 RestTemplate,因为我不想连接到外部服务——我知道期望的响应。我尝试在集成测试包中提供不同的实现,希望后者会覆盖真正的实现,但检查日志却是相反的:真正的实现覆盖了测试。
如何确定 TestConfig 中的那个是使用的那个?
这是我的配置文件:
@Configuration
public class RestTemplateProvider {
private static final int DEFAULT_SERVICE_TIMEOUT = 5_000;
@Bean
public RestTemplate restTemplate(){
return new RestTemplate(buildClientConfigurationFactory());
}
private ClientHttpRequestFactory buildClientConfigurationFactory() {
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setReadTimeout(DEFAULT_SERVICE_TIMEOUT);
factory.setConnectTimeout(DEFAULT_SERVICE_TIMEOUT);
return factory;
}
}
集成测试:
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = TestConfiguration.class)
@WebAppConfiguration
@ActiveProfiles("it")
public abstract class IntegrationTest {}
测试配置class:
@Configuration
@Import({Application.class, MockRestTemplateConfiguration.class})
public class TestConfiguration {}
最后是 MockRestTemplateConfiguration
@Configuration
public class MockRestTemplateConfiguration {
@Bean
public RestTemplate restTemplate() {
return Mockito.mock(RestTemplate.class)
}
}
1。
您可以使用 @Primary
注释:
@Configuration
public class MockRestTemplateConfiguration {
@Bean
@Primary
public RestTemplate restTemplate() {
return Mockito.mock(RestTemplate.class)
}
}
顺便说一句,我写了blog post about faking Spring bean
2。 但我建议看一下 Spring RestTemplate testing support。这将是一个简单的例子:
private MockRestServiceServer mockServer;
@Autowired
private RestTemplate restTemplate;
@Autowired
private UsersClient usersClient;
@BeforeMethod
public void init() {
mockServer = MockRestServiceServer.createServer(restTemplate);
}
@Test
public void testSingleGet() throws Exception {
// GIVEN
int testingIdentifier = 0;
mockServer.expect(requestTo(USERS_URL + "/" + testingIdentifier))
.andExpect(method(HttpMethod.GET))
.andRespond(withSuccess(TEST_RECORD0, MediaType.APPLICATION_JSON));
// WHEN
User user = usersClient.getUser(testingIdentifier);
// THEN
mockServer.verify();
assertEquals(user.getName(), USER0_NAME);
assertEquals(user.getEmail(), USER0_EMAIL);
}
中找到更多示例
自 Spring Boot 1.4.x 有一个选项可以使用 @MockBean
注释来伪造 Spring bean。
对评论的反应:
要在缓存中保留上下文,请不要使用 @DirtiesContext
,但使用 @ContextConfiguration(name = "contextWithFakeBean")
它将创建单独的上下文,同时它会将默认上下文保留在缓存中。 Spring 会将两者(或您有多少上下文)保存在缓存中。
我们的构建是这样的,其中大多数测试都使用默认的非污染配置,但我们有 4-5 个测试是伪造的 beans。默认上下文被很好地重用
再深入一点,看我的第二个回答。
我使用
解决了问题@SpringBootTest(classes = {AppConfiguration.class, AppTestConfiguration.class})
而不是
@Import({ AppConfiguration.class, AppTestConfiguration.class });
在我的例子中,测试与应用程序不在同一个包中。所以我需要明确指定 AppConfiguration.class(或 App.class)。如果你在测试中使用相同的包,我想你可以只写
@SpringBootTest(classes = AppTestConfiguration.class)
而不是(不工作)
@Import(AppTestConfiguration.class );
看到这如此不同,真是太棒了。也许有人可以解释这一点。直到现在我找不到任何好的答案。您可能会认为,如果 @SpringBootTests
存在,则 @Import(...)
不会被拾取,但是在日志中显示了覆盖 bean。只是方向错了。
顺便说一下,使用 @TestConfiguration
代替 @Configuration
也没有区别。
您配置中的问题是您正在使用 @Configuration
作为您的测试配置。这将替换您的主要配置。而是使用 @TestConfiguration
这将附加(覆盖)您的主要配置。
46.3.2 Detecting Test Configuration
If you want to customize the primary configuration, you can use a nested @TestConfiguration class. Unlike a nested @Configuration class, which would be used instead of your application’s primary configuration, a nested @TestConfiguration class is used in addition to your application’s primary configuration.
使用 SpringBoot 的示例:
主要class
@SpringBootApplication() // Will scan for @Components and @Configs in package tree
public class Main{
}
主要配置
@Configuration
public void AppConfig() {
// Define any beans
}
测试配置
@TestConfiguration
public void AppTestConfig(){
// override beans for testing
}
测试class
@RunWith(SpringRunner.class)
@Import(AppTestConfig.class)
@SpringBootTest
public void AppTest() {
// use @MockBean if you like
}
注意:请注意,所有 Bean 都将被创建,即使是您覆盖的那些。如果您不想实例化 @Configuration
.
@Profile
检查
@MockBean
和 OP 使用的 bean 覆盖是两种互补的方法。
您想使用 @MockBean
创建模拟并忘记真正的实现:通常您这样做是为了切片测试或集成测试,它们不会加载某些 class(es) 的 bean您正在测试依赖并且 您不想在集成中测试这些 bean。
Spring 默认使它们成为 null
,您将模拟它们的最小行为以完成您的测试。
@WebMvcTest
经常需要该策略,因为您不想测试整个层,并且 @SpringBootTest
也可能需要,如果您在测试配置中仅指定 beans 配置的一个子集.
另一方面,有时您希望使用尽可能多的真实组件执行集成测试,因此您不想使用 @MockBean
但您希望稍微覆盖一个行为,一个依赖项或者为 bean 定义一个新的范围,在这种情况下,要遵循的方法是 bean 覆盖:
@SpringBootTest({"spring.main.allow-bean-definition-overriding=true"})
@Import(FooTest.OverrideBean.class)
public class FooTest{
@Test
public void getFoo() throws Exception {
// ...
}
@TestConfiguration
public static class OverrideBean {
// change the bean scope to SINGLETON
@Bean
@Scope(ConfigurableBeanFactory.SINGLETON)
public Bar bar() {
return new Bar();
}
// use a stub for a bean
@Bean
public FooBar BarFoo() {
return new BarFooStub();
}
// use a stub for the dependency of a bean
@Bean
public FooBar fooBar() {
return new FooBar(new StubDependency());
}
}
}
使用@Primary 注释,Bean 覆盖与 Spring Boot 1.5.X 一起工作,但在 Spring Boot 2.1.X 下失败,它抛出错误:
Invalid bean definition with name 'testBean' defined in sample..ConfigTest$SpringConfig:..
There is already .. defined in class path resource [TestConfig.class]] bound
请在下面添加 properties=
,这将明确指示 Spring 允许覆盖,这是不言自明的。
@SpringBootTest(properties = ["spring.main.allow-bean-definition-overriding=true"])
更新:您可以在 application-test.yml 中添加相同的 属性(文件名取决于您正在测试的测试配置文件名称)
我在测试中声明了一个内部配置 class 因为我只想覆盖一个方法
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class FileNotificationWebhookTest{
public static class FileNotificationWebhookTestConfiguration {
@Bean
@Primary
public FileJobRequestConverter fileJobRequestConverter() {
return new FileJobRequestConverter() {
@Override
protected File resolveWindowsPath(String path) {
return new File(path);
}
};
}
}
}
然而,
在 @SpringBootTest 中声明配置无效:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,classes = {FileNotificationWebhookTest.FileNotificationWebhookTestConfiguration.class})
或使用 @Configuration 注释测试配置无效:
@Configuration
public static class FileNotificationWebhookTestConfiguration {
}
并导致
Caused by: org.springframework.context.ApplicationContextException: Unable to start web server; nested exception is org.springframework.context.ApplicationContextException: Unable to start ServletWebServerApplicationContext due to missing ServletWebServerFactory bean.
对我有用的(与这里的其他一些帖子相反)是使用@Import
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Import(FileNotificationWebhookTest.FileNotificationWebhookTestConfiguration.class)
class FileNotificationWebhookTest {
}
使用 Spring:5.3.3 和 Spring-Boot-Starter:2.4.2
@MockBean
创建 Mockito 模拟而不是生产构建。
如果您不想使用 Mockito,但以其他方式提供替代品(即通过功能切换禁用 bean 的某些功能),我建议使用 @TestConfiguration
的组合(因为 Spring Boot 1.4.0) 和 @Primary
注释。
@TestConfiguration
将加载您的默认上下文并应用您的 @TestConfiguration
片段。添加 @Primary
将强制将模拟的 RestTemplate 注入到它的依赖项中。
请参阅下面的简化示例:
@SpringBootTest
public class ServiceTest {
@TestConfiguration
static class AdditionalCfg {
@Primary
@Bean
RestTemplate rt() {
return new RestTemplate() {
@Override
public String exec() {
return "Test rest template";
}
};
}
}
@Autowired
MyService myService;
@Test
void contextLoads() {
assertThat(myService.invoke()).isEqualTo("Test rest template");
}
}
我找到的最简单的解决方案是在 application.properties 中设置这个 属性:
spring.main.allow-bean-definition-overriding=true
这将启用 bean 的覆盖。
接下来,在测试中创建一个配置 class,并用以下内容注释您的 bean:
@Bean
@Primary
这样,当 运行 测试时,这个 bean 将覆盖您常用的 bean。