unit-test-config-properties

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Unit Testing Configuration Properties and Profiles

配置属性与Profile的单元测试

Test @ConfigurationProperties bindings, environment-specific configurations, and property validation using JUnit 5. Verify configuration loading without full Spring context startup.
使用JUnit 5测试@ConfigurationProperties绑定、环境特定配置以及属性校验。无需启动完整Spring上下文即可验证配置加载情况。

When to Use This Skill

适用场景

Use this skill when:
  • Testing @ConfigurationProperties property binding
  • Testing property name mapping and type conversions
  • Verifying configuration validation
  • Testing environment-specific configurations
  • Testing nested property structures
  • Want fast configuration tests without Spring context
在以下场景中使用本技能:
  • 测试@ConfigurationProperties属性绑定
  • 测试属性名称映射与类型转换
  • 验证配置校验规则
  • 测试环境特定配置
  • 测试嵌套属性结构
  • 希望在不启动Spring上下文的情况下快速执行配置测试

Setup: Configuration Testing

配置测试环境搭建

Maven

Maven

xml
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-configuration-processor</artifactId>
  <scope>provided</scope>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.assertj</groupId>
  <artifactId>assertj-core</artifactId>
  <scope>test</scope>
</dependency>
xml
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-configuration-processor</artifactId>
  <scope>provided</scope>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.assertj</groupId>
  <artifactId>assertj-core</artifactId>
  <scope>test</scope>
</dependency>

Gradle

Gradle

kotlin
dependencies {
  annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
  testImplementation("org.springframework.boot:spring-boot-starter-test")
  testImplementation("org.junit.jupiter:junit-jupiter")
  testImplementation("org.assertj:assertj-core")
}
kotlin
dependencies {
  annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
  testImplementation("org.springframework.boot:spring-boot-starter-test")
  testImplementation("org.junit.jupiter:junit-jupiter")
  testImplementation("org.assertj:assertj-core")
}

Basic Pattern: Testing ConfigurationProperties

基础测试模式:测试ConfigurationProperties

Simple Property Binding

简单属性绑定

java
// Configuration properties class
@ConfigurationProperties(prefix = "app.security")
@Data
public class SecurityProperties {
  private String jwtSecret;
  private long jwtExpirationMs;
  private int maxLoginAttempts;
  private boolean enableTwoFactor;
}

// Unit test
import org.junit.jupiter.api.Test;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import static org.assertj.core.api.Assertions.*;

class SecurityPropertiesTest {

  @Test
  void shouldBindPropertiesFromEnvironment() {
    new ApplicationContextRunner()
      .withPropertyValues(
        "app.security.jwtSecret=my-secret-key",
        "app.security.jwtExpirationMs=3600000",
        "app.security.maxLoginAttempts=5",
        "app.security.enableTwoFactor=true"
      )
      .withBean(SecurityProperties.class)
      .run(context -> {
        SecurityProperties props = context.getBean(SecurityProperties.class);

        assertThat(props.getJwtSecret()).isEqualTo("my-secret-key");
        assertThat(props.getJwtExpirationMs()).isEqualTo(3600000L);
        assertThat(props.getMaxLoginAttempts()).isEqualTo(5);
        assertThat(props.isEnableTwoFactor()).isTrue();
      });
  }

  @Test
  void shouldUseDefaultValuesWhenPropertiesNotProvided() {
    new ApplicationContextRunner()
      .withPropertyValues("app.security.jwtSecret=key")
      .withBean(SecurityProperties.class)
      .run(context -> {
        SecurityProperties props = context.getBean(SecurityProperties.class);

        assertThat(props.getJwtSecret()).isEqualTo("key");
        assertThat(props.getMaxLoginAttempts()).isZero();
      });
  }
}
java
// Configuration properties class
@ConfigurationProperties(prefix = "app.security")
@Data
public class SecurityProperties {
  private String jwtSecret;
  private long jwtExpirationMs;
  private int maxLoginAttempts;
  private boolean enableTwoFactor;
}

// Unit test
import org.junit.jupiter.api.Test;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import static org.assertj.core.api.Assertions.*;

class SecurityPropertiesTest {

  @Test
  void shouldBindPropertiesFromEnvironment() {
    new ApplicationContextRunner()
      .withPropertyValues(
        "app.security.jwtSecret=my-secret-key",
        "app.security.jwtExpirationMs=3600000",
        "app.security.maxLoginAttempts=5",
        "app.security.enableTwoFactor=true"
      )
      .withBean(SecurityProperties.class)
      .run(context -> {
        SecurityProperties props = context.getBean(SecurityProperties.class);

        assertThat(props.getJwtSecret()).isEqualTo("my-secret-key");
        assertThat(props.getJwtExpirationMs()).isEqualTo(3600000L);
        assertThat(props.getMaxLoginAttempts()).isEqualTo(5);
        assertThat(props.isEnableTwoFactor()).isTrue();
      });
  }

  @Test
  void shouldUseDefaultValuesWhenPropertiesNotProvided() {
    new ApplicationContextRunner()
      .withPropertyValues("app.security.jwtSecret=key")
      .withBean(SecurityProperties.class)
      .run(context -> {
        SecurityProperties props = context.getBean(SecurityProperties.class);

        assertThat(props.getJwtSecret()).isEqualTo("key");
        assertThat(props.getMaxLoginAttempts()).isZero();
      });
  }
}

Testing Nested Configuration Properties

测试嵌套配置属性

Complex Property Structure

复杂属性结构

java
@ConfigurationProperties(prefix = "app.database")
@Data
public class DatabaseProperties {
  private String url;
  private String username;
  private Pool pool = new Pool();
  private List<Replica> replicas = new ArrayList<>();

  @Data
  public static class Pool {
    private int maxSize = 10;
    private int minIdle = 5;
    private long connectionTimeout = 30000;
  }

  @Data
  public static class Replica {
    private String name;
    private String url;
    private int priority;
  }
}

class NestedPropertiesTest {

  @Test
  void shouldBindNestedProperties() {
    new ApplicationContextRunner()
      .withPropertyValues(
        "app.database.url=jdbc:mysql://localhost/db",
        "app.database.username=admin",
        "app.database.pool.maxSize=20",
        "app.database.pool.minIdle=10",
        "app.database.pool.connectionTimeout=60000"
      )
      .withBean(DatabaseProperties.class)
      .run(context -> {
        DatabaseProperties props = context.getBean(DatabaseProperties.class);

        assertThat(props.getUrl()).isEqualTo("jdbc:mysql://localhost/db");
        assertThat(props.getPool().getMaxSize()).isEqualTo(20);
        assertThat(props.getPool().getConnectionTimeout()).isEqualTo(60000L);
      });
  }

  @Test
  void shouldBindListOfReplicas() {
    new ApplicationContextRunner()
      .withPropertyValues(
        "app.database.replicas[0].name=replica-1",
        "app.database.replicas[0].url=jdbc:mysql://replica1/db",
        "app.database.replicas[0].priority=1",
        "app.database.replicas[1].name=replica-2",
        "app.database.replicas[1].url=jdbc:mysql://replica2/db",
        "app.database.replicas[1].priority=2"
      )
      .withBean(DatabaseProperties.class)
      .run(context -> {
        DatabaseProperties props = context.getBean(DatabaseProperties.class);

        assertThat(props.getReplicas()).hasSize(2);
        assertThat(props.getReplicas().get(0).getName()).isEqualTo("replica-1");
        assertThat(props.getReplicas().get(1).getPriority()).isEqualTo(2);
      });
  }
}
java
@ConfigurationProperties(prefix = "app.database")
@Data
public class DatabaseProperties {
  private String url;
  private String username;
  private Pool pool = new Pool();
  private List<Replica> replicas = new ArrayList<>();

  @Data
  public static class Pool {
    private int maxSize = 10;
    private int minIdle = 5;
    private long connectionTimeout = 30000;
  }

  @Data
  public static class Replica {
    private String name;
    private String url;
    private int priority;
  }
}

class NestedPropertiesTest {

  @Test
  void shouldBindNestedProperties() {
    new ApplicationContextRunner()
      .withPropertyValues(
        "app.database.url=jdbc:mysql://localhost/db",
        "app.database.username=admin",
        "app.database.pool.maxSize=20",
        "app.database.pool.minIdle=10",
        "app.database.pool.connectionTimeout=60000"
      )
      .withBean(DatabaseProperties.class)
      .run(context -> {
        DatabaseProperties props = context.getBean(DatabaseProperties.class);

        assertThat(props.getUrl()).isEqualTo("jdbc:mysql://localhost/db");
        assertThat(props.getPool().getMaxSize()).isEqualTo(20);
        assertThat(props.getPool().getConnectionTimeout()).isEqualTo(60000L);
      });
  }

  @Test
  void shouldBindListOfReplicas() {
    new ApplicationContextRunner()
      .withPropertyValues(
        "app.database.replicas[0].name=replica-1",
        "app.database.replicas[0].url=jdbc:mysql://replica1/db",
        "app.database.replicas[0].priority=1",
        "app.database.replicas[1].name=replica-2",
        "app.database.replicas[1].url=jdbc:mysql://replica2/db",
        "app.database.replicas[1].priority=2"
      )
      .withBean(DatabaseProperties.class)
      .run(context -> {
        DatabaseProperties props = context.getBean(DatabaseProperties.class);

        assertThat(props.getReplicas()).hasSize(2);
        assertThat(props.getReplicas().get(0).getName()).isEqualTo("replica-1");
        assertThat(props.getReplicas().get(1).getPriority()).isEqualTo(2);
      });
  }
}

Testing Property Validation

测试属性校验

Validate Configuration with Constraints

通过约束校验配置

java
@ConfigurationProperties(prefix = "app.server")
@Data
@Validated
public class ServerProperties {
  @NotBlank
  private String host;

  @Min(1)
  @Max(65535)
  private int port = 8080;

  @Positive
  private int threadPoolSize;

  @Email
  private String adminEmail;
}

class ConfigurationValidationTest {

  @Test
  void shouldFailValidationWhenHostIsBlank() {
    new ApplicationContextRunner()
      .withPropertyValues(
        "app.server.host=",
        "app.server.port=8080",
        "app.server.threadPoolSize=10"
      )
      .withBean(ServerProperties.class)
      .run(context -> {
        assertThat(context).hasFailed()
          .getFailure()
          .hasMessageContaining("host");
      });
  }

  @Test
  void shouldFailValidationWhenPortOutOfRange() {
    new ApplicationContextRunner()
      .withPropertyValues(
        "app.server.host=localhost",
        "app.server.port=99999",
        "app.server.threadPoolSize=10"
      )
      .withBean(ServerProperties.class)
      .run(context -> {
        assertThat(context).hasFailed();
      });
  }

  @Test
  void shouldPassValidationWithValidConfiguration() {
    new ApplicationContextRunner()
      .withPropertyValues(
        "app.server.host=localhost",
        "app.server.port=8080",
        "app.server.threadPoolSize=10",
        "app.server.adminEmail=admin@example.com"
      )
      .withBean(ServerProperties.class)
      .run(context -> {
        assertThat(context).hasNotFailed();
        ServerProperties props = context.getBean(ServerProperties.class);
        assertThat(props.getHost()).isEqualTo("localhost");
      });
  }
}
java
@ConfigurationProperties(prefix = "app.server")
@Data
@Validated
public class ServerProperties {
  @NotBlank
  private String host;

  @Min(1)
  @Max(65535)
  private int port = 8080;

  @Positive
  private int threadPoolSize;

  @Email
  private String adminEmail;
}

class ConfigurationValidationTest {

  @Test
  void shouldFailValidationWhenHostIsBlank() {
    new ApplicationContextRunner()
      .withPropertyValues(
        "app.server.host=",
        "app.server.port=8080",
        "app.server.threadPoolSize=10"
      )
      .withBean(ServerProperties.class)
      .run(context -> {
        assertThat(context).hasFailed()
          .getFailure()
          .hasMessageContaining("host");
      });
  }

  @Test
  void shouldFailValidationWhenPortOutOfRange() {
    new ApplicationContextRunner()
      .withPropertyValues(
        "app.server.host=localhost",
        "app.server.port=99999",
        "app.server.threadPoolSize=10"
      )
      .withBean(ServerProperties.class)
      .run(context -> {
        assertThat(context).hasFailed();
      });
  }

  @Test
  void shouldPassValidationWithValidConfiguration() {
    new ApplicationContextRunner()
      .withPropertyValues(
        "app.server.host=localhost",
        "app.server.port=8080",
        "app.server.threadPoolSize=10",
        "app.server.adminEmail=admin@example.com"
      )
      .withBean(ServerProperties.class)
      .run(context -> {
        assertThat(context).hasNotFailed();
        ServerProperties props = context.getBean(ServerProperties.class);
        assertThat(props.getHost()).isEqualTo("localhost");
      });
  }
}

Testing Profile-Specific Configurations

测试Profile特定配置

Environment-Specific Properties

环境特定属性

java
@Configuration
@Profile("prod")
class ProductionConfiguration {
  @Bean
  public SecurityProperties securityProperties() {
    SecurityProperties props = new SecurityProperties();
    props.setEnableTwoFactor(true);
    props.setMaxLoginAttempts(3);
    return props;
  }
}

@Configuration
@Profile("dev")
class DevelopmentConfiguration {
  @Bean
  public SecurityProperties securityProperties() {
    SecurityProperties props = new SecurityProperties();
    props.setEnableTwoFactor(false);
    props.setMaxLoginAttempts(999);
    return props;
  }
}

class ProfileBasedConfigurationTest {

  @Test
  void shouldLoadProductionConfiguration() {
    new ApplicationContextRunner()
      .withPropertyValues("spring.profiles.active=prod")
      .withUserConfiguration(ProductionConfiguration.class)
      .run(context -> {
        SecurityProperties props = context.getBean(SecurityProperties.class);

        assertThat(props.isEnableTwoFactor()).isTrue();
        assertThat(props.getMaxLoginAttempts()).isEqualTo(3);
      });
  }

  @Test
  void shouldLoadDevelopmentConfiguration() {
    new ApplicationContextRunner()
      .withPropertyValues("spring.profiles.active=dev")
      .withUserConfiguration(DevelopmentConfiguration.class)
      .run(context -> {
        SecurityProperties props = context.getBean(SecurityProperties.class);

        assertThat(props.isEnableTwoFactor()).isFalse();
        assertThat(props.getMaxLoginAttempts()).isEqualTo(999);
      });
  }
}
java
@Configuration
@Profile("prod")
class ProductionConfiguration {
  @Bean
  public SecurityProperties securityProperties() {
    SecurityProperties props = new SecurityProperties();
    props.setEnableTwoFactor(true);
    props.setMaxLoginAttempts(3);
    return props;
  }
}

@Configuration
@Profile("dev")
class DevelopmentConfiguration {
  @Bean
  public SecurityProperties securityProperties() {
    SecurityProperties props = new SecurityProperties();
    props.setEnableTwoFactor(false);
    props.setMaxLoginAttempts(999);
    return props;
  }
}

class ProfileBasedConfigurationTest {

  @Test
  void shouldLoadProductionConfiguration() {
    new ApplicationContextRunner()
      .withPropertyValues("spring.profiles.active=prod")
      .withUserConfiguration(ProductionConfiguration.class)
      .run(context -> {
        SecurityProperties props = context.getBean(SecurityProperties.class);

        assertThat(props.isEnableTwoFactor()).isTrue();
        assertThat(props.getMaxLoginAttempts()).isEqualTo(3);
      });
  }

  @Test
  void shouldLoadDevelopmentConfiguration() {
    new ApplicationContextRunner()
      .withPropertyValues("spring.profiles.active=dev")
      .withUserConfiguration(DevelopmentConfiguration.class)
      .run(context -> {
        SecurityProperties props = context.getBean(SecurityProperties.class);

        assertThat(props.isEnableTwoFactor()).isFalse();
        assertThat(props.getMaxLoginAttempts()).isEqualTo(999);
      });
  }
}

Testing Type Conversion

测试类型转换

Property Type Binding

属性类型绑定

java
@ConfigurationProperties(prefix = "app.features")
@Data
public class FeatureProperties {
  private Duration cacheExpiry = Duration.ofMinutes(10);
  private DataSize maxUploadSize = DataSize.ofMegabytes(100);
  private List<String> enabledFeatures;
  private Map<String, String> featureFlags;
  private Charset fileEncoding = StandardCharsets.UTF_8;
}

class TypeConversionTest {

  @Test
  void shouldConvertStringToDuration() {
    new ApplicationContextRunner()
      .withPropertyValues("app.features.cacheExpiry=30s")
      .withBean(FeatureProperties.class)
      .run(context -> {
        FeatureProperties props = context.getBean(FeatureProperties.class);

        assertThat(props.getCacheExpiry()).isEqualTo(Duration.ofSeconds(30));
      });
  }

  @Test
  void shouldConvertStringToDataSize() {
    new ApplicationContextRunner()
      .withPropertyValues("app.features.maxUploadSize=50MB")
      .withBean(FeatureProperties.class)
      .run(context -> {
        FeatureProperties props = context.getBean(FeatureProperties.class);

        assertThat(props.getMaxUploadSize()).isEqualTo(DataSize.ofMegabytes(50));
      });
  }

  @Test
  void shouldConvertCommaDelimitedListToList() {
    new ApplicationContextRunner()
      .withPropertyValues("app.features.enabledFeatures=feature1,feature2,feature3")
      .withBean(FeatureProperties.class)
      .run(context -> {
        FeatureProperties props = context.getBean(FeatureProperties.class);

        assertThat(props.getEnabledFeatures())
          .containsExactly("feature1", "feature2", "feature3");
      });
  }
}
java
@ConfigurationProperties(prefix = "app.features")
@Data
public class FeatureProperties {
  private Duration cacheExpiry = Duration.ofMinutes(10);
  private DataSize maxUploadSize = DataSize.ofMegabytes(100);
  private List<String> enabledFeatures;
  private Map<String, String> featureFlags;
  private Charset fileEncoding = StandardCharsets.UTF_8;
}

class TypeConversionTest {

  @Test
  void shouldConvertStringToDuration() {
    new ApplicationContextRunner()
      .withPropertyValues("app.features.cacheExpiry=30s")
      .withBean(FeatureProperties.class)
      .run(context -> {
        FeatureProperties props = context.getBean(FeatureProperties.class);

        assertThat(props.getCacheExpiry()).isEqualTo(Duration.ofSeconds(30));
      });
  }

  @Test
  void shouldConvertStringToDataSize() {
    new ApplicationContextRunner()
      .withPropertyValues("app.features.maxUploadSize=50MB")
      .withBean(FeatureProperties.class)
      .run(context -> {
        FeatureProperties props = context.getBean(FeatureProperties.class);

        assertThat(props.getMaxUploadSize()).isEqualTo(DataSize.ofMegabytes(50));
      });
  }

  @Test
  void shouldConvertCommaDelimitedListToList() {
    new ApplicationContextRunner()
      .withPropertyValues("app.features.enabledFeatures=feature1,feature2,feature3")
      .withBean(FeatureProperties.class)
      .run(context -> {
        FeatureProperties props = context.getBean(FeatureProperties.class);

        assertThat(props.getEnabledFeatures())
          .containsExactly("feature1", "feature2", "feature3");
      });
  }
}

Testing Property Binding with Default Values

测试带默认值的属性绑定

Verify Default Configuration

验证默认配置

java
@ConfigurationProperties(prefix = "app.cache")
@Data
public class CacheProperties {
  private long ttlSeconds = 300;
  private int maxSize = 1000;
  private boolean enabled = true;
  private String cacheType = "IN_MEMORY";
}

class DefaultValuesTest {

  @Test
  void shouldUseDefaultValuesWhenNotSpecified() {
    new ApplicationContextRunner()
      .withBean(CacheProperties.class)
      .run(context -> {
        CacheProperties props = context.getBean(CacheProperties.class);

        assertThat(props.getTtlSeconds()).isEqualTo(300L);
        assertThat(props.getMaxSize()).isEqualTo(1000);
        assertThat(props.isEnabled()).isTrue();
        assertThat(props.getCacheType()).isEqualTo("IN_MEMORY");
      });
  }

  @Test
  void shouldOverrideDefaultValuesWithProvidedProperties() {
    new ApplicationContextRunner()
      .withPropertyValues(
        "app.cache.ttlSeconds=600",
        "app.cache.cacheType=REDIS"
      )
      .withBean(CacheProperties.class)
      .run(context -> {
        CacheProperties props = context.getBean(CacheProperties.class);

        assertThat(props.getTtlSeconds()).isEqualTo(600L);
        assertThat(props.getCacheType()).isEqualTo("REDIS");
        assertThat(props.getMaxSize()).isEqualTo(1000); // Default unchanged
      });
  }
}
java
@ConfigurationProperties(prefix = "app.cache")
@Data
public class CacheProperties {
  private long ttlSeconds = 300;
  private int maxSize = 1000;
  private boolean enabled = true;
  private String cacheType = "IN_MEMORY";
}

class DefaultValuesTest {

  @Test
  void shouldUseDefaultValuesWhenNotSpecified() {
    new ApplicationContextRunner()
      .withBean(CacheProperties.class)
      .run(context -> {
        CacheProperties props = context.getBean(CacheProperties.class);

        assertThat(props.getTtlSeconds()).isEqualTo(300L);
        assertThat(props.getMaxSize()).isEqualTo(1000);
        assertThat(props.isEnabled()).isTrue();
        assertThat(props.getCacheType()).isEqualTo("IN_MEMORY");
      });
  }

  @Test
  void shouldOverrideDefaultValuesWithProvidedProperties() {
    new ApplicationContextRunner()
      .withPropertyValues(
        "app.cache.ttlSeconds=600",
        "app.cache.cacheType=REDIS"
      )
      .withBean(CacheProperties.class)
      .run(context -> {
        CacheProperties props = context.getBean(CacheProperties.class);

        assertThat(props.getTtlSeconds()).isEqualTo(600L);
        assertThat(props.getCacheType()).isEqualTo("REDIS");
        assertThat(props.getMaxSize()).isEqualTo(1000); // Default unchanged
      });
  }
}

Best Practices

最佳实践

  • Test all property bindings including nested structures
  • Test validation constraints thoroughly
  • Test both default and custom values
  • Use ApplicationContextRunner for context-free testing
  • Test profile-specific configurations separately
  • Verify type conversions work correctly
  • Test edge cases (empty strings, null values, type mismatches)
  • 测试所有属性绑定,包括嵌套结构
  • 全面测试校验约束
  • 测试默认值与自定义值
  • 使用ApplicationContextRunner进行无上下文测试
  • 单独测试Profile特定配置
  • 验证类型转换是否正常工作
  • 测试边缘场景(空字符串、空值、类型不匹配)

Common Pitfalls

常见误区

  • Not testing validation constraints
  • Forgetting to test default values
  • Not testing nested property structures
  • Testing with wrong property prefix
  • Not handling type conversion properly
  • 未测试校验约束
  • 忘记测试默认值
  • 未测试嵌套属性结构
  • 使用错误的属性前缀进行测试
  • 未正确处理类型转换

Troubleshooting

故障排查

Properties not binding: Verify prefix and property names match exactly (including kebab-case to camelCase conversion).
Validation not triggered: Ensure
@Validated
is present and validation dependencies are on classpath.
ApplicationContextRunner not found: Verify
spring-boot-starter-test
is in test dependencies.
属性绑定失败:确认前缀与属性名称完全匹配(包括短横线命名到驼峰命名的转换)。
校验未触发:确保已添加
@Validated
注解,且校验依赖已在类路径中。
ApplicationContextRunner未找到:确认
spring-boot-starter-test
已添加到测试依赖中。

References

参考资料