在测试应用程序上下文中在真正的自动配置 bean 和存根 bean 之间进行选择的方法

问题描述

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

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

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

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

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

解决方法

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

,

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

ContextCustomizerFactory 中注册一个 META-INF/spring.factories,这取决于当前测试类上的某个标记,要么注册一个 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());
    }
  }
}