【问题标题】:Way to select between real autoconfigured bean and a stub bean in test application context在测试应用程序上下文中在真正的自动配置 bean 和存根 bean 之间进行选择的方法
【发布时间】:2021-03-23 19:19:47
【问题描述】:

我想使用存根 bean 运行大部分 Spring Boot 测试(它基本上什么都不做),但有些测试实际上应该使用真正的 Spring Boot 的自动配置 bean。如何在默认情况下将存根 bean 放在测试上下文中,但有条件地将其切换为真正的 bean?

我所有的测试都来自几个提供正确测试设置的超类,而这个特定的 bean 设置与之正交,所以我不能

  • 创建两个新的/不同的超类,一个用于存根,一个用于真实的
  • 在测试类中使用@MockBean,因为大多数时候我想要存根,不需要记得添加这个
  • 在超类中使用 @MockBean,因为我不知道如何在我想要 Spring Boot 自动配置模拟的特定测试类中覆盖模拟。

我正在寻找一个简单易用的解决方案,例如 JUnit-Rule;测试类上的注释以及 TestExecutionListener;或其他类似的东西。

请注意,真正的 bean 是一个完全自动配置的 bean,我在我的生产代码中没有任何配置(仅在 application.properties 中) - 它实际上是 JavaMailSender 但那个细节应该无关紧要.

【问题讨论】:

    标签: java spring-boot unit-testing spring-boot-test


    【解决方案1】:

    我会为此使用不同的配置文件。您可以使用@ActiveProfiles("myprofile") 或您想用于该测试的任何配置文件来注释您的测试类。然后添加两个不同的 bean 配置并在那里指定配置文件 (@Profile("myprofile"))。

    【讨论】:

    • 谢谢。我已经有一个测试配置文件,我一直希望它处于活动状态。现在正如我的问题中提到的,在我的一个场景中,我不想定义任何自定义 bean(以触发 Spring Boot 的自动配置 bean 配置)。因此,经过一番思考,虽然您的解决方案是可行的,但在我看来会有点混乱,我需要在配置文件中配置带有否定配置文件或更复杂的布尔逻辑的配置 - 似乎也不是很容易维护我。
    • 哦。好的。在您的问题中,您只谈论“一颗豆子”。如果是很多豆子,那么这真的会变得一团糟。
    【解决方案2】:

    不是很容易发现,深入到Spring TestContext Framework中,但这是解决问题中需求的解决方案:

    META-INF/spring.factories 中注册ContextCustomizerFactory,这取决于当前测试类上的某些标记,要么注册ContextCustomizer,要么不注册。 ContextCustomizer 又注册了一个 BeanFactoryPostProcessor 从 BeanRegistry 中删除原始生产 bean 并添加模拟 bean。

    所以它的工作方式与问题中的相反。默认情况下,它不会将模拟 bean 放在上下文中,而是将生产配置加载为默认配置,并在最后根据需要交换不需要的生产 bean。

    在测试资源中添加META-INF/spring.factories

    org.springframework.test.context.ContextCustomizerFactory=my.package.MyContextCustomizerFactory
    

    将这些类添加到您的测试源中:

    public class MyContextCustomizerFactory implements ContextCustomizerFactory {
    
      @Override
      public ContextCustomizer createContextCustomizer(
          Class<?> testClass, List<ContextConfigurationAttributes> configAttributes) {
        
        // Your logic to decide based on the testClass whether you want the real bean or the mock.
        // See other ContextCustomizerFactories provided by Spring for how you can look at fields and annotations on the test class etc.
        boolean shouldSwapBeanWithMock = ...
    
        if (shouldSwapBeanWithMock) {
          return new MyContextCustomizer();
        } else {
          return null;
        }
      }
    
      private static class MyContextCustomizer implements ContextCustomizer {
    
        /**
         * ContextCustomizers are called very early in the application context lifecycle, where not all
         * beans are registered yet. This is why we must register first a {@link
         * BeanFactoryPostProcessor} which will do the bean swap, instead of doing it in the Customizer
         * directly.
         */
        @Override
        public void customizeContext(
            ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
          context.addBeanFactoryPostProcessor(new MyBeanFactoryPostProcessor());
        }
    
        /**
         * ContextCustomizers are part of the key which decides if an ApplicationContext can be
         * retrieved from cache or needs to be freshly instantiated. {@code equals} and {@code hashCode}
         * are overridden for that reason, because multiple instances of this Customizer are equivalent.
         * The cache key should only depend on whether this customizer was present in the context or
         * not.
         */
        @Override
        public boolean equals(Object obj) {
          return (obj != null) && (obj.getClass() == getClass());
        }
    
        @Override
        public int hashCode() {
          return getClass().hashCode();
        }
    
      }
    
      private static class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    
        @Override
        public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
          // select whichever bean type you want to swap here
          String[] beansToSwap = beanFactory.getBeanNamesForType(JavaMailSender.class, false, false);
    
          Arrays.stream(beansToSwap)
              .forEach(((BeanDefinitionRegistry) beanFactory)::removeBeanDefinition);
    
          // register your mock bean as singleton or however you like
          beanFactory.registerSingleton(MyMockMailSender.class.getName(), new MyMockMailSender());
        }
      }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-07-27
      • 2019-07-18
      • 1970-01-01
      • 2014-10-30
      • 1970-01-01
      • 1970-01-01
      • 2023-04-10
      • 1970-01-01
      相关资源
      最近更新 更多