1. 概述

Spring Boot 的自动配置(Auto-Configuration)是其核心特性之一,但要测试这些自动配置的场景并不容易。尤其是当你需要模拟不同类路径、Bean 或配置属性存在与否的情况时。

幸运的是,Spring Boot 提供了一个非常实用的测试工具类:ApplicationContextRunner。它可以帮助我们快速构建和测试一个定制的 ApplicationContext,并通过类似 AssertJ 的链式断言方式来验证结果。

本文将通过几个典型场景,演示如何使用 ApplicationContextRunner 测试不同条件下的自动配置行为。


2. 测试自动配置场景

ApplicationContextRunner 是一个用于运行 ApplicationContext 并提供链式断言的工具类。推荐将其作为测试类的字段进行初始化,便于在多个测试方法中复用:

private final ApplicationContextRunner contextRunner = new ApplicationContextRunner();

接下来我们将通过几个典型条件测试来展示它的使用方式。

2.1. 测试类是否存在(@ConditionalOnClass / @ConditionalOnMissingClass)

这两个注解用于控制某个配置类是否生效,取决于某个类是否存在于类路径中。

我们先定义两个配置类:

@Configuration
@ConditionalOnClass(ConditionalOnClassIntegrationTest.class)
protected static class ConditionalOnClassConfiguration {
    @Bean
    public String created() {
        return "This is created when ConditionalOnClassIntegrationTest is present on the classpath";
    }
}

@Configuration
@ConditionalOnMissingClass("com.baeldung.autoconfiguration.ConditionalOnClassIntegrationTest")
protected static class ConditionalOnMissingClassConfiguration {
    @Bean
    public String missed() {
        return "This is missed when ConditionalOnClassIntegrationTest is present on the classpath";
    }
}

接下来使用 ApplicationContextRunner 测试这些配置是否按预期生效:

类存在时 Bean 被创建

@Test
public void whenDependentClassIsPresent_thenBeanCreated() {
    this.contextRunner.withUserConfiguration(ConditionalOnClassConfiguration.class)
        .run(context -> {
            assertThat(context).hasBean("created");
            assertThat(context.getBean("created")).isEqualTo("This is created when ConditionalOnClassIntegrationTest is present on the classpath");
        });
}

类存在时 Bean 不应被创建

@Test
public void whenDependentClassIsPresent_thenBeanMissing() {
    this.contextRunner.withUserConfiguration(ConditionalOnMissingClassConfiguration.class)
        .run(context -> {
            assertThat(context).doesNotHaveBean("missed");
        });
}

类不存在时 Bean 不应被创建

这里就要用到 FilteredClassLoader,它可以模拟类路径中某些类不存在的情况:

@Test
public void whenDependentClassIsNotPresent_thenBeanMissing() {
    this.contextRunner.withUserConfiguration(ConditionalOnClassConfiguration.class)
        .withClassLoader(new FilteredClassLoader(ConditionalOnClassIntegrationTest.class))
        .run(context -> {
            assertThat(context).doesNotHaveBean("created");
            assertThat(context).doesNotHaveBean(ConditionalOnClassIntegrationTest.class);
        });
}

类不存在时 Bean 应该被创建

@Test
public void whenDependentClassIsNotPresent_thenBeanCreated() {
    this.contextRunner.withUserConfiguration(ConditionalOnMissingClassConfiguration.class)
        .withClassLoader(new FilteredClassLoader(ConditionalOnClassIntegrationTest.class))
        .run(context -> {
            assertThat(context).hasBean("missed");
            assertThat(context.getBean("missed")).isEqualTo("This is missed when ConditionalOnClassIntegrationTest is present on the classpath");
            assertThat(context).doesNotHaveBean(ConditionalOnClassIntegrationTest.class);
        });
}

2.2. 测试 Bean 是否存在(@ConditionalOnBean / @ConditionalOnMissingBean)

这两个注解用于根据上下文中是否存在某个 Bean 来决定是否创建新 Bean。

我们定义如下配置类:

@Configuration
protected static class BasicConfiguration {
    @Bean
    public String created() {
        return "This is always created";
    }
}

@Configuration
@ConditionalOnBean(name = "created")
protected static class ConditionalOnBeanConfiguration {
    @Bean
    public String createOnBean() {
        return "This is created when bean (name=created) is present";
    }
}

@Configuration
@ConditionalOnMissingBean(name = "created")
protected static class ConditionalOnMissingBeanConfiguration {
    @Bean
    public String createOnMissingBean() {
        return "This is created when bean (name=created) is missing";
    }
}

然后测试这些配置:

依赖 Bean 存在时,目标 Bean 被创建

@Test
public void whenDependentBeanIsPresent_thenConditionalBeanCreated() {
    this.contextRunner.withUserConfiguration(
        BasicConfiguration.class, 
        ConditionalOnBeanConfiguration.class
    ).run(context -> {
        assertThat(context).hasBean("createOnBean");
        assertThat(context.getBean("createOnBean")).isEqualTo("This is created when bean (name=created) is present");
    });
}

依赖 Bean 不存在时,目标 Bean 被创建

@Test
public void whenDependentBeanIsNotPresent_thenConditionalMissingBeanCreated() {
    this.contextRunner.withUserConfiguration(ConditionalOnMissingBeanConfiguration.class)
        .run(context -> {
            assertThat(context).hasBean("createOnMissingBean");
            assertThat(context.getBean("createOnMissingBean")).isEqualTo("This is created when bean (name=created) is missing");
        });
}

2.3. 测试属性值(@ConditionalOnProperty)

该注解用于根据配置属性值决定是否启用某个 Bean。

我们先定义一个配置类:

@Configuration
@TestPropertySource("classpath:ConditionalOnPropertyTest.properties")
protected static class SimpleServiceConfiguration {
    @Bean
    @ConditionalOnProperty(name = "com.baeldung.service", havingValue = "default")
    @ConditionalOnMissingBean
    public DefaultService defaultService() {
        return new DefaultService();
    }

    @Bean
    @ConditionalOnProperty(name = "com.baeldung.service", havingValue = "custom")
    @ConditionalOnMissingBean
    public CustomService customService() {
        return new CustomService();
    }
}

然后使用 withPropertyValues 方法模拟不同的配置值:

配置值为 custom 时,CustomService 被创建

@Test
public void whenGivenCustomPropertyValue_thenCustomServiceCreated() {
    this.contextRunner.withPropertyValues("com.baeldung.service=custom")
        .withUserConfiguration(SimpleServiceConfiguration.class)
        .run(context -> {
            assertThat(context).hasBean("customService");
            SimpleService simpleService = context.getBean(CustomService.class);
            assertThat(simpleService.serve()).isEqualTo("Custom Service");
            assertThat(context).doesNotHaveBean("defaultService");
        });
}

配置值为 default 时,DefaultService 被创建

@Test
public void whenGivenDefaultPropertyValue_thenDefaultServiceCreated() {
    this.contextRunner.withPropertyValues("com.baeldung.service=default")
        .withUserConfiguration(SimpleServiceConfiguration.class)
        .run(context -> {
            assertThat(context).hasBean("defaultService");
            SimpleService simpleService = context.getBean(DefaultService.class);
            assertThat(simpleService.serve()).isEqualTo("Default Service");
            assertThat(context).doesNotHaveBean("customService");
        });
}

3. 小结

通过以上几个典型示例,我们展示了如何使用 ApplicationContextRunner 简单粗暴地测试 Spring Boot 自动配置的常见场景:

  • 类是否存在(@ConditionalOnClass / @ConditionalOnMissingClass)
  • Bean 是否存在(@ConditionalOnBean / @ConditionalOnMissingBean)
  • 属性值是否匹配(@ConditionalOnProperty)

⚠️ 注意ApplicationContextRunner 适用于非 Web 应用的测试。如果是基于 Servlet 的 Web 应用,应使用 WebApplicationContextRunner;如果是响应式 Web 应用,则应使用 ReactiveWebApplicationContextRunner

完整的示例代码可在 GitHub 上找到。


原始标题:Guide to ApplicationContextRunner in Spring Boot | Baeldung