在集成测试中覆盖 bean

Posted

技术标签:

【中文标题】在集成测试中覆盖 bean【英文标题】:Overriding beans in Integration tests 【发布时间】:2016-06-15 01:15:00 【问题描述】:

对于我的 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 

TestConfiguration 类:

@Configuration
@Import(Application.class, MockRestTemplateConfiguration.class)
public class TestConfiguration 

最后是 MockRestTemplateConfiguration

@Configuration
public class MockRestTemplateConfiguration 

    @Bean
    public RestTemplate restTemplate() 
        return Mockito.mock(RestTemplate.class)
    

【问题讨论】:

切换导入的顺序,按照读取的方式解析,所以后面的会覆盖前面的。 试过了......同样的事情。我会更新我的问题以反映变化 Overriding an Autowired Bean in Unit Tests的可能重复 【参考方案1】:

从 Spring Boot 1.4.x 开始,可以选择使用 @MockBean 注释来伪造 Spring bean。

评论反应:

要将上下文保存在缓存中,不要使用@DirtiesContext,而是使用@ContextConfiguration(name = "contextWithFakeBean"),它将创建单独的上下文,同时将默认上下文保存在缓存中。 Spring 会将两者(或您拥有的多少个上下文)都保存在缓存中。

我们的构建是这样的,其中大多数测试都使用默认的非污染配置,但我们有 4-5 个测试是伪造的 bean。默认上下文很好地重用

【讨论】:

同意这是在大多数情况下要走的路。我发现这种方法的缺点是您将在下一个测试类中失去上下文缓存的好处。为了使我们的集成测试尽可能快,我们尽可能扩展一个类。 是的,但有时别无他法。例如。当您需要伪造外部服务时。 注解为@MockBean,见docs.spring.io/spring-boot/docs/current/reference/html/… 这是一个很好的解决方案,但是我的问题不能这样解决,因为我在做AOP安全相关的测试。 如果您使用 AOP 代理,我建议您查看我的其他答案,其中我链接了 Github 存储库。该存储库包含如何模拟 AOP 代理 Spring bean 的示例。【参考方案2】:

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

更多例子可以在my Github repo here找到

【讨论】:

我尝试了第二个建议,效果很好。感谢您的提示..我觉得这是“正确”的方式。 谢谢。选项 1 在我的 TestMock Bean 上使用简单的 @Primary 注释对我有用,它优先于我的 Test 类中的字段 @Autowire 的生产注释。除了这个@Primary 注释之外,不需要进行其他更改。 这个(#1)应该被接受的答案。如此简单 请注意,当使用@Primary 时,这不会阻止创建两个 bean。也就是说,@Primary 不适合当您不想执行某些 @PostConstruct 初始化代码时。 当生产代码包含多个此类 bean 并且其中一个已标记为 @Primary(而其他使用 @Qualifier)时,此解决方案不起作用。这种解决方案本质上滥用了它不打算用于的消歧机制。【参考方案3】:

您的配置中的问题是您使用@Configuration 进行测试配置。这将替换您的主要配置。而是使用 @TestConfiguration,它将附加(覆盖)您的主要配置。

46.3.2 Detecting Test Configuration

如果要自定义主配置,可以使用 嵌套的@TestConfiguration 类。与嵌套的 @Configuration 类不同, 这将被用来代替你的应用程序的主要 配置,另外使用嵌套的@TestConfiguration 类 到您的应用程序的主要配置。

使用 SpringBoot 的示例:

主类

@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
 

测试类

@RunWith(SpringRunner.class)
@Import(AppTestConfig.class)
@SpringBootTest
public void AppTest() 
    // use @MockBean if you like

注意:请注意,所有 Bean 都将被创建,即使是您覆盖的那些。如果您不想实例化 @Configuration,请使用 @Profile

【讨论】:

免责声明:早期的 Spring 版本不支持此注解,自 Spring Boot 1.4.0 起可用 那么,如果甚至会创建被覆盖的 bean,“覆盖”是什么意思?【参考方案4】:

@MockBean 和 OP 使用的 bean 覆盖是两种互补的方法。

您想使用@MockBean 创建一个模拟并忘记真正的实现:通常您这样做是为了切片测试或集成测试,它不会加载您正在测试的类所依赖的某些bean,并且 您不想在集成中测试这些 bean。 Spring默认使它们null,您将模拟它们的最小行为以完成您的测试。

@WebMvcTest 经常需要该策略,因为您不想测试整个层,@SpringBootTest 也可能需要,如果您在测试配置中仅指定 bean 配置的子集。

另一方面,有时您希望使用尽可能多的真实组件执行集成测试,因此您不想使用@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());
        

    

【讨论】:

【参考方案5】:

再深入一点,看我的第二个答案

我用

解决了这个问题
@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 也没有区别。

【讨论】:

我的情况正好相反【参考方案6】:

使用 @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 中添加相同的属性(文件名取决于您使用的测试配置文件名称)

【讨论】:

【参考方案7】:

我在测试中声明了一个内部配置类,因为我只想覆盖一个方法

@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 


并导致

引起:org.springframework.context.ApplicationContextException: 无法启动网络服务器;嵌套异常是 org.springframework.context.ApplicationContextException:无法 由于缺少启动 ServletWebServerApplicationContext 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

【讨论】:

【参考方案8】:

检查this 答案以及该线程中提供的其他答案。 这是关于在 Spring Boot 2.X 中覆盖 bean,默认情况下禁用此选项。如果你决定走这条路,它也有一些关于如何使用 Bean 定义 DSL 的想法。

【讨论】:

【参考方案9】:

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


【讨论】:

以上是关于在集成测试中覆盖 bean的主要内容,如果未能解决你的问题,请参考以下文章

在集成测试中覆盖spring @Configuration

MockMVC 将测试控制器与会话范围 bean 集成

集成测试覆盖率工具 codecov

我想在 Spring Boot 集成测试中创建 bean 之前模拟服务器

如何为基于 http 的集成测试生成覆盖率报告?

单元测试在测试spring集成TCP组件时创建名为“amqAdmin”的bean时出错