简体   繁体   English

在集成测试中覆盖 bean

[英]Overriding beans in Integration tests

For my Spring-Boot app I provide a RestTemplate though a @Configuration file so I can add sensible defaults(ex Timeouts).对于我的 Spring-Boot 应用程序,我通过 @Configuration 文件提供了一个 RestTemplate,以便我可以添加合理的默认值(例如超时)。 For my integration tests I would like to mock the RestTemplate as I dont want to connect to external services - I know what responses to expect.对于我的集成测试,我想模拟 RestTemplate,因为我不想连接到外部服务 - 我知道期望什么响应。 I tried providing a different implementation in the integration-test package in the hope that the latter will override the real implementation, but checking the logs it`s the other way around: the real implementation overrides the test one.我尝试在集成测试 package 中提供不同的实现,希望后者会覆盖真正的实现,但检查日志却是另一种方式:真正的实现会覆盖测试。

How can I make sure the one from the TestConfig is the one used?如何确保 TestConfig 中的那个是使用的那个?

This is my config file:这是我的配置文件:

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

Integration test:集成测试:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = TestConfiguration.class)
@WebAppConfiguration
@ActiveProfiles("it")
public abstract class IntegrationTest {}

TestConfiguration class:测试配置 class:

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

And finally MockRestTemplateConfiguration最后是 MockRestTemplateConfiguration

@Configuration
public class MockRestTemplateConfiguration {

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

Since Spring Boot 1.4.x there is an option to use @MockBean annotation to fake Spring beans.从 Spring Boot 1.4.x 开始,可以选择使用@MockBean注释来伪造 Spring bean。

Reaction on comment:对评论的反应:

To keep context in cache do not use @DirtiesContext , but use @ContextConfiguration(name = "contextWithFakeBean") and it will create separate context, while it will keep default context in cache.要在缓存中保留上下文,请不要使用@DirtiesContext ,而是使用@ContextConfiguration(name = "contextWithFakeBean")它将创建单独的上下文,同时将默认上下文保留在缓存中。 Spring will keep both (or how many contexts you have) in cache. Spring 会将两者(或您拥有的上下文数量)都保存在缓存中。

Our build is this way, where most of the tests are using default non-poluted config, but we have 4-5 tests that are faking beans.我们的构建是这样的,其中大多数测试使用默认的无污染配置,但我们有 4-5 个测试是伪造的 bean。 Default context is nicely reused默认上下文被很好地重用

1. You can use @Primary annotation: 1.你可以使用@Primary注解:

@Configuration
public class MockRestTemplateConfiguration {

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

BTW, I wrote blog post about faking Spring bean顺便说一句,我写了一篇关于伪造 Spring bean 的博客文章

2. But I would suggest to take a look at Spring RestTemplate testing support . 2. 但我建议看一下Spring RestTemplate testing support This would be simple example:这将是一个简单的例子:

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

More examples can be found in my Github repo here更多示例可以在我的 Github 仓库中找到

The Problem in your configuration is that you are using @Configuration for your test configuration.您的配置中的问题是您使用@Configuration作为您的测试配置。 This will replace your main configuration.这将替换您的主要配置。 Instead use @TestConfiguration which will append (override) your main configuration.而是使用@TestConfiguration ,它将附加(覆盖)您的主要配置。

46.3.2 Detecting Test Configuration 46.3.2 检测测试配置

If you want to customize the primary configuration, you can use a nested @TestConfiguration class.如果要自定义主要配置,可以使用嵌套的 @TestConfiguration 类。 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.与嵌套的 @Configuration 类不同,它将用于代替应用程序的主要配置,除了应用程序的主要配置之外,还使用嵌套的 @TestConfiguration 类。

Example using SpringBoot:使用 SpringBoot 的示例:

Main class主班

@SpringBootApplication() // Will scan for @Components and @Configs in package tree
public class Main{
}

Main config主要配置

@Configuration
public void AppConfig() { 
    // Define any beans
}

Test config测试配置

@TestConfiguration
public void AppTestConfig(){
    // override beans for testing
} 

Test class测试班

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

Note: Be aware, that all Beans will be created, even those that you override.注意:请注意,所有 Bean 都将被创建,即使是您覆盖的 Bean。 Use @Profile if you wish not to instantiate a @Configuration .如果您不想实例化@Configuration请使用@Profile

Getting a little deeper into it, see my second answer .更深入一点,请参阅我的第二个答案

I solved the Problem using我使用解决了问题

@SpringBootTest(classes = {AppConfiguration.class, AppTestConfiguration.class})

instead of代替

@Import({ AppConfiguration.class, AppTestConfiguration.class });

In my case the Test is not in the same package as the App.在我的情况下,测试与应用程序不在同一个包中。 So I need to specify the AppConfiguration.class (or the App.class) explicit.所以我需要明确指定 AppConfiguration.class(或 App.class)。 If you use the same package in the test, than I guess you could just write如果你在测试中使用相同的包,我猜你可以写

@SpringBootTest(classes = AppTestConfiguration.class)

instead of (not working)而不是(不工作)

@Import(AppTestConfiguration.class );

It is pretty wired to see that this is so different.看到这如此不同,真是令人难以置信。 Maybe some one can explain this.也许有人可以解释这一点。 I could not find any good answers until now.直到现在我找不到任何好的答案。 You might think, @Import(...) is not picked up if @SpringBootTests is present, but in the log the overriding bean shows up.您可能会认为,如果@SpringBootTests存在,则不会接收@SpringBootTests @Import(...) ,但在日志中会显示覆盖 bean。 But just the wrong way around.但只是方法错误。

By the way, using @TestConfiguration instead @Configuration also makes no difference.顺便说一句,使用@TestConfiguration代替@Configuration也没有区别。

@MockBean and bean overriding used by the OP are two complementary approaches. @MockBean和 OP 使用的 bean 覆盖是两种互补的方法。

You want to use @MockBean to create a mock and forget the real implementation : generally you do that for slice testing or integration testing that doesn't load some beans which class(es) you are testing depend on and that you don't want to test these beans in integration .您想使用@MockBean创建一个模拟并忘记真正的实现:通常,您这样做是为了切片测试或集成测试,这些测试不会加载您正在测试的类所依赖且您不想要的某些 bean在集成中测试这些 bean
Spring makes them by default null , you will mock the minimal behavior for them to fulfill your test. Spring 默认使它们为null ,您将模拟它们的最小行为来完成您的测试。

@WebMvcTest requires very often that strategy as you don't want to test the whole layers and @SpringBootTest may also require that if you specify only a subset of your beans configuration in the test configuration. @WebMvcTest经常需要这种策略,因为您不想测试整个层,如果您在测试配置中仅指定 bean 配置的一个子集, @SpringBootTest也可能需要该策略。

On the other hand, sometimes you want to perform an integration test with as many real components as possible, so you don't want to use @MockBean but you want to override slightly a behavior, a dependency or define a new scope for a bean, in this case, the approach to follow is bean overriding :另一方面,有时您希望使用尽可能多的真实组件执行集成测试,因此您不想使用@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());
        }

    }
}

With @Primary annotation, Bean overriding works with Spring Boot 1.5.X but fails with Spring Boot 2.1.X it throw error:使用@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

Please add below properties= which will instruct Spring explicitly to allow overriding, it is self explainatory.请在下面添加properties=这将明确指示 Spring 允许覆盖,这是不言自明的。

@SpringBootTest(properties = ["spring.main.allow-bean-definition-overriding=true"])

UPDATE: You can add the same property in application-test.yml (file name depend upon what test profile name you are tests with)更新:您可以在 application-test.yml 中添加相同的属性(文件名取决于您测试的测试配置文件名称)

I´ve declared an inner configuration class within my test because I wanted to overwrite just a single method我在测试中声明了一个内部配置类,因为我只想覆盖一个方法

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

However,然而,

Declaring the configuration in @SpringBootTest did not work :@SpringBootTest 中声明配置不起作用

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,classes = {FileNotificationWebhookTest.FileNotificationWebhookTestConfiguration.class})

or annotating the test configuration with @Configuration did not work :或使用@Configuration注释测试配置不起作用

@Configuration
public static class FileNotificationWebhookTestConfiguration {

}

and was leading to并导致

Caused by: org.springframework.context.ApplicationContextException: Unable to start web server;引起:org.springframework.context.ApplicationContextException:无法启动web服务器; nested exception is org.springframework.context.ApplicationContextException: Unable to start ServletWebServerApplicationContext due to missing ServletWebServerFactory bean.嵌套异常是 org.springframework.context.ApplicationContextException: Unable to start ServletWebServerApplicationContext 由于缺少 ServletWebServerFactory bean。

What did work for me ( contrary to some other posts here) was using @Import什么对我有用(与这里的其他一些帖子相反)是使用@Import

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Import(FileNotificationWebhookTest.FileNotificationWebhookTestConfiguration.class)
class FileNotificationWebhookTest {

}

Using Spring: 5.3.3 with Spring-Boot-Starter: 2.4.2使用 Spring:5.3.3 和 Spring-Boot-Starter:2.4.2

Check this answer along with others provided in that thread.检查答案以及该线程中提供的其他答案。 It's about overriding bean in Spring Boot 2.X, where this option was disabled by default.这是关于覆盖 Spring Boot 2.X 中的 bean,默认情况下禁用此选项。 It also has some ideas about how to use Bean Definition DSL if you decided to take that path.如果您决定走这条路,它也有一些关于如何使用 Bean Definition DSL 的想法。

@MockBean creates Mockito mock instead of production build. @MockBean创建 Mockito 模拟而不是生产构建。

If you do not want to use Mockito, but provide a replacement in some other way (ie by disabling some features of bean with feature toggles), I suggest using combination of @TestConfiguration (since Spring Boot 1.4.0) and @Primary annotation.如果您不想使用 Mockito,而是以其他方式提供替代品(即通过功能切换禁用 bean 的某些功能),我建议使用@TestConfiguration (自 Spring Boot 1.4.0 起)和@Primary注释的组合。

@TestConfiguration will load your default context and apply your @TestConfiguration piece in addition to it. @TestConfiguration将加载您的默认上下文并应用您的@TestConfiguration部分。 Adding @Primary will force your mocked RestTemplate to be injected to it's dependents.添加@Primary将强制将您模拟的 RestTemplate 注入到它的依赖项中。

See simplified example below:请参阅下面的简化示例:

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

The simplest solution I found was to set this property in application.properties:我找到的最简单的解决方案是在 application.properties 中设置这个属性:

spring.main.allow-bean-definition-overriding=true

This will enable overriding of beans.这将启用 bean 的覆盖。

Next, create a configuration class in test, and annotate your bean with:接下来,在测试中创建配置 class,并使用以下命令注释您的 bean:

@Bean
@Primary

This way, this bean will override your usual bean when running tests.这样,这个 bean 将在运行测试时覆盖您通常的 bean。

This is super weird.这太奇怪了。

In my case, (Spring Boot 2.6.7), I could simply @Import MyTestConfiguration containing a custom @Primary @Bean into my @SpringBootTest, and everything worked.在我的情况下,(Spring Boot 2.6.7),我可以简单地将包含自定义@Primary @Bean 的@Import MyTestConfiguration 放入我的@SpringBootTest,一切正常。

Right until I needed to explicitly name my bean.直到我需要明确命名我的 bean。 Then I suddenly had to resort to然后我突然不得不求助于

@SpringBootTest(
    properties = ["spring.main.allow-bean-definition-overriding=true"],
    classes = [MyTestConfig::class],
)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM