在集成测试中覆盖Bean

126

我为我的Spring-Boot应用程序提供了一个RestTemplate,通过@Configuration文件来添加合理的默认值(例如超时)。在我的集成测试中,我想要模拟RestTemplate,因为我不想连接到外部服务 - 我知道希望得到哪些响应。我尝试在integration-test包中提供一个不同的实现,希望后者会覆盖真正的实现,但是检查日志发现相反的情况:真正的实现覆盖了测试的实现。

如何确保使用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 {}

测试配置类:

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

最后是MockRestTemplateConfiguration

@Configuration
public class MockRestTemplateConfiguration {

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

1
将导入语句的顺序调换一下,它们会按照被读取到的顺序进行解析,所以后面的导入会覆盖前面的导入。 - M. Deinum
尝试过了...还是一样。我会更新我的问题以反映这些更改。 - mvlupan
可能是在单元测试中覆盖Autowired Bean的问题的重复。 - LoganMzz
12个回答

57

1. 你可以使用 @Primary 注解:

@Configuration
public class MockRestTemplateConfiguration {

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

顺便提一下,我写了一篇关于模拟Spring bean的博客文章

2.但我建议看看Spring RestTemplate测试支持。这是一个简单的示例:

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

更多示例可以在这里的我的Github资料库找到。


2
我尝试了第二个建议,效果非常好。感谢提示。我觉得这是“正确”的方法。 - mvlupan
2
谢谢。对我来说,选项1很有效,只需在我的TestMock Bean上添加一个简单的@Primary注释即可,这将优先于Test类中字段的@Autowire生产环境。除了这个@Primary注释外,不需要进行任何其他更改。 - DaddyMoe
1
请注意,使用 @Primary 时,这并不会阻止两个 bean 被创建。也就是说,在存在一些您不想执行的 @PostConstruct 初始化代码时,@Primary 不适用。 - Rens Verhage
3
当生产代码包含多个这样的bean并且其中一个已经标记为@Primary(而其他使用@Qualifier)时,此解决方案将无法工作。该解决方案本质上滥用了消歧机制,这并不是其预期用途。 - Markus
Marcus,这个答案是Spring Boot引入@MockedBean注解之前的一种解决方法。因此,这个答案已经过时了,你应该遵循被接受的答案。 - luboskrnac
显示剩余2条评论

57

从Spring Boot 1.4.x开始,有一个使用@MockBean注解来伪造Spring bean的选项。

对评论的回应:

为了保持上下文缓存,请不要使用@DirtiesContext,而是使用@ContextConfiguration(name = "contextWithFakeBean"),它会创建一个单独的上下文,同时会将默认上下文保留在缓存中。Spring将保存所有(或你所拥有的)上下文在缓存中。

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


1
大多数情况下,我同意这是正确的方法。我发现这种方法的缺点是,在下一个测试类中,您将失去上下文缓存的好处。为了使我们的集成测试尽可能快,我们尽可能地扩展一个类。 - mvlupan
1
是的,有时候只能这样做。例如,当您需要模拟外部服务时。 - luboskrnac
如果您使用AOP代理,我建议查看我的其他答案,其中我链接了Github存储库。该存储库包含如何模拟AOP代理的Spring bean的示例。 - luboskrnac
这个解决方案很难应用,当模拟的bean作为参数传递时,你不知道会调用哪些方法。例如,如果你需要mock java.time.Clock到Clock.fixed()实例。 - WeGa
@WeGa,我不理解你的评论。我建议创建一个带有确切示例的单独问题,有人会直接回答你的示例。我相信使用MockedBean注释是可能的。 - luboskrnac
显示剩余3条评论

43
您的配置问题在于您正在使用@Configuration来进行测试配置。这将替换您的主要配置。相反,请使用@TestConfiguration,它将追加(覆盖)您的主要配置。 46.3.2 检测测试配置

如果您想自定义主要配置,则可以使用嵌套的@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都将被创建,即使您覆盖了某些Bean。如果您不希望实例化@Configuration,请使用@Profile


免责声明:此注释不支持早期的Spring版本,仅在Spring Boot 1.4.0及以上版本中可用。 - eugen-fried
13
如果即使被覆盖的bean也会被创建,那么“override”是什么意思? - Ilya Serbis
我认为AppConfigAppTestConfig应该是类,而不是方法。例如:@Configuration public class AppConfig { // 定义任何bean } - Tim Büthe
3
它的工作非常好,但是如果你要覆盖bean,自从Spring Boot 2.1以来,默认情况下禁用了bean覆盖功能,你需要使用属性:"spring.main.allow-bean-definition-overriding=true"。 - coderazzi

25

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


这对我帮助很大,谢谢! - Jakub Moravec
4
在以下代码中,[]和{}哪一个是正确的: @SpringBootTest(properties = ["spring.main.allow-bean-definition-overriding=true"]) - Tasos Zervos

23

@MockBean和bean覆盖是两种互补的方法。您可以使用@MockBean创建一个模拟对象,而忘记实际实现:通常情况下,您会为了片段测试或集成测试而这么做,这些测试不会加载某些类所依赖的bean,并且您不想在集成中测试这些bean
Spring默认将它们设置为null,您将模拟最小行为以满足测试要求。

@WebMvcTest经常需要这种策略,因为您不希望测试整个层次结构,如果在测试配置中仅指定了一部分bean配置,则@SpringBootTest也可能需要。

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

    }
}

20

深入了解一下,请看我的第二个答案(see my second answer)

我使用以下方法解决了这个问题:

@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也没有任何区别。


1
在我的情况下,情况正好相反。 - Marian Klühspies
你需要添加 spring.main.allow-bean-definition-overriding=true - JyotiKumarPoddar

5

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

@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: 无法启动Web服务器;嵌套异常是 org.springframework.context.ApplicationContextException: 由于缺少 ServletWebServerFactory bean,无法启动ServletWebServerApplicationContext。

对我有效的方法(与此处某些帖子相反)是使用@Import

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

}

使用Spring: 5.3.3与Spring-Boot-Starter: 2.4.2。

4

@MockBean会创建Mockito mock对象,而不是生产构建。

如果您不想使用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");
    }
}


这对我起作用了,我不知道为什么,这就是Spring的魔力。 - kiltek

4

这很奇怪。

在我的情况下(Spring Boot 2.6.7),我可以简单地引入一个包含自定义@Primary @Bean的MyTestConfiguration到我的@SpringBootTest中,然后一切都正常工作。

直到我需要显式命名我的Bean。 然后我突然不得不采取...

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

1
我找到的最简单的解决方案是在(测试) application.properties 中设置此属性:
spring.main.allow-bean-definition-overriding=true

这将启用覆盖bean。

接下来,在测试中创建一个配置类,并使用以下注释对您的bean进行注释:

@Bean
@Primary

这样,当运行测试时,此 Bean 将覆盖您通常使用的 Bean。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接