测试 spring 应用程序上下文无法启动的最佳方法是什么?
What is the best way to test that a spring application context fails to start?
我使用 spring-boot-starter-web 和 spring-boot-starter-test。
假设我有一个 class 用于绑定配置属性:
@ConfigurationProperties(prefix = "dummy")
public class DummyProperties {
@URL
private String url;
// getter, setter ...
}
现在我想测试我的bean验证是否正确。如果未设置 属性 dummy.value
或包含无效的 URL,则上下文应该无法启动(并显示特定错误消息)。如果 属性 包含有效的 URL,上下文应该开始。 (测试会显示缺少 @NotNull
。)
测试 class 看起来像这样:
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = MyApplication.class)
@IntegrationTest({ "dummy.url=123:456" })
public class InvalidUrlTest {
// my test code
}
此测试将失败,因为提供的 属性 无效。告诉 Spring/JUnit 的最佳方式是什么:"yep, this error is expected"。在普通的 JUnit 测试中,我会使用 ExpectedException。
我认为最简单的方法是:
public class InvalidUrlTest {
@Rule
public DisableOnDebug testTimeout = new DisableOnDebug(new Timeout(5, TimeUnit.SECONDS));
@Rule
public ExpectedException expected = ExpectedException.none();
@Test
public void shouldFailOnStartIfUrlInvalid() {
// configure ExpectedException
expected.expect(...
MyApplication.main("--dummy.url=123:456");
}
// other cases
}
为什么首先要进行集成测试?你为什么要为此启动一个完整的 Spring 启动应用程序?
我觉得这像是单元测试。话虽这么说,你有几个选择:
- 不要添加
@IntegrationTest
和 Spring 启动将不会启动 Web 服务器(使用 @PropertySource
将值传递给您的测试,但通过一个整个测试的无效值 class)
- 您可以使用
spring.main.web-environment=false
来禁用网络服务器(但鉴于上述观点,这很愚蠢)
- 编写一个单元测试来处理您的
DummyProperties
。您甚至不需要为此启动 Spring 启动应用程序。看看our own test suite
我肯定会选择最后一个。也许您有充分的理由为此进行集成测试?
测试Spring应用上下文的最好方法是使用ApplicationContextRunner
在Spring引导参考文档中有描述:
https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-developing-auto-configuration.html#boot-features-test-autoconfig
并且有一个关于它的快速指南:
https://www.baeldung.com/spring-boot-context-runner
示例用法
private static final String POSITIVE_CASE_CONFIG_FILE =
"classpath:some/path/positive-case-config.yml";
private static final String NEGATIVE_CASE_CONFIG_FILE =
"classpath:some/path/negative-case-config.yml";
@Test
void positiveTest() {
ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withInitializer(new ConfigDataApplicationContextInitializer())//1
.withInitializer(new ConditionEvaluationReportLoggingListener(LogLevel.DEBUG))//2
.withUserConfiguration(MockBeansTestConfiguration.class)//3
.withPropertyValues("spring.config.location=" + POSITIVE_CASE_CONFIG_FILE)//4
.withConfiguration(AutoConfigurations.of(BookService.class));//5
contextRunner
.run((context) -> {
Assertions.assertThat(context).hasNotFailed();//6
});
}
@Test
void negativeTest() {
ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withInitializer(new ConfigDataApplicationContextInitializer())//1
.withInitializer(new ConditionEvaluationReportLoggingListener(LogLevel.DEBUG))//2
.withUserConfiguration(MockBeansTestConfiguration.class)//3
.withPropertyValues("spring.config.location=" + NEGATIVE_CASE_CONFIG_FILE)//4
.withConfiguration(AutoConfigurations.of(BookService.class));//5
contextRunner
.run((context) -> {
assertThat(context)
.hasFailed();
assertThat(context.getStartupFailure())
.isNotNull();
assertThat(context.getStartupFailure().getMessage())
.contains("Some exception message");
assertThat(extractFailureCauseMessages(context))
.contains("Cause exception message");
});
}
private List<String> extractFailureCauseMessages(AssertableApplicationContext context) {
var failureCauseMessages = new ArrayList<String>();
var currentCause = context.getStartupFailure().getCause();
while (!Objects.isNull(currentCause)) {//7
failureCauseMessages.add(currentCause.getMessage());
currentCause = currentCause.getCause();
}
return failureCauseMessages;
}
使用 Junit5 中类似定义的示例进行解释 Spring 引导测试注释:
- 触发加载配置文件,例如
application.properties
或 application.yml
- 当应用程序上下文失败时使用给定的日志级别记录
ConditionEvaluationReport
- 提供指定模拟 bean 的 class,即。我们在
BookService
中有 @Autowired BookRepository
,在 MockBeansTestConfiguration
中提供模拟 BookRepository
。类似于测试 class 中的 @Import({MockBeansTestConfiguration.class})
和 class 中的 @TestConfiguration
以及普通 Junit5 中的模拟 bean Spring 启动测试
- 相当于
@TestPropertySource(properties = { "spring.config.location=" + POSITIVE_CASE_CONFIG_FILE})
- 触发 spring 给定 class 的自动配置,不是直接等效的,但它类似于使用
@ContextConfiguration(classes = {BookService.class})
或 @SpringBootTest(classes = {BookService.class})
和 @Import({BookService.class})
正常测试
- Assertions.class 来自 AssertJ 库,
Assertions.assertThat
应该有静态导入,但我想显示此方法的来源
Objects.isNull
应该有静态导入,但我想显示此方法的来源
MockBeansTestConfigurationclass:
@TestConfiguration
public class MockBeansTestConfiguration {
private static final Book SAMPLE_BOOK = Book.of(1L, "Stanisław Lem", "Solaris", "978-3-16-148410-0");
@Bean
public BookRepository mockBookRepository() {
var bookRepository = Mockito.mock(BookRepository.class);//1
Mockito.when(bookRepository.findByIsbn(SAMPLE_BOOK.getIsbn()))//2
.thenReturn(SAMPLE_BOOK);
return bookRepository;
}
}
备注:
1,2。应该有静态导入,但我想显示这个方法来自哪里
我使用 spring-boot-starter-web 和 spring-boot-starter-test。
假设我有一个 class 用于绑定配置属性:
@ConfigurationProperties(prefix = "dummy")
public class DummyProperties {
@URL
private String url;
// getter, setter ...
}
现在我想测试我的bean验证是否正确。如果未设置 属性 dummy.value
或包含无效的 URL,则上下文应该无法启动(并显示特定错误消息)。如果 属性 包含有效的 URL,上下文应该开始。 (测试会显示缺少 @NotNull
。)
测试 class 看起来像这样:
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = MyApplication.class)
@IntegrationTest({ "dummy.url=123:456" })
public class InvalidUrlTest {
// my test code
}
此测试将失败,因为提供的 属性 无效。告诉 Spring/JUnit 的最佳方式是什么:"yep, this error is expected"。在普通的 JUnit 测试中,我会使用 ExpectedException。
我认为最简单的方法是:
public class InvalidUrlTest {
@Rule
public DisableOnDebug testTimeout = new DisableOnDebug(new Timeout(5, TimeUnit.SECONDS));
@Rule
public ExpectedException expected = ExpectedException.none();
@Test
public void shouldFailOnStartIfUrlInvalid() {
// configure ExpectedException
expected.expect(...
MyApplication.main("--dummy.url=123:456");
}
// other cases
}
为什么首先要进行集成测试?你为什么要为此启动一个完整的 Spring 启动应用程序?
我觉得这像是单元测试。话虽这么说,你有几个选择:
- 不要添加
@IntegrationTest
和 Spring 启动将不会启动 Web 服务器(使用@PropertySource
将值传递给您的测试,但通过一个整个测试的无效值 class) - 您可以使用
spring.main.web-environment=false
来禁用网络服务器(但鉴于上述观点,这很愚蠢) - 编写一个单元测试来处理您的
DummyProperties
。您甚至不需要为此启动 Spring 启动应用程序。看看our own test suite
我肯定会选择最后一个。也许您有充分的理由为此进行集成测试?
测试Spring应用上下文的最好方法是使用ApplicationContextRunner
在Spring引导参考文档中有描述:
https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-developing-auto-configuration.html#boot-features-test-autoconfig
并且有一个关于它的快速指南:
https://www.baeldung.com/spring-boot-context-runner
示例用法
private static final String POSITIVE_CASE_CONFIG_FILE =
"classpath:some/path/positive-case-config.yml";
private static final String NEGATIVE_CASE_CONFIG_FILE =
"classpath:some/path/negative-case-config.yml";
@Test
void positiveTest() {
ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withInitializer(new ConfigDataApplicationContextInitializer())//1
.withInitializer(new ConditionEvaluationReportLoggingListener(LogLevel.DEBUG))//2
.withUserConfiguration(MockBeansTestConfiguration.class)//3
.withPropertyValues("spring.config.location=" + POSITIVE_CASE_CONFIG_FILE)//4
.withConfiguration(AutoConfigurations.of(BookService.class));//5
contextRunner
.run((context) -> {
Assertions.assertThat(context).hasNotFailed();//6
});
}
@Test
void negativeTest() {
ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withInitializer(new ConfigDataApplicationContextInitializer())//1
.withInitializer(new ConditionEvaluationReportLoggingListener(LogLevel.DEBUG))//2
.withUserConfiguration(MockBeansTestConfiguration.class)//3
.withPropertyValues("spring.config.location=" + NEGATIVE_CASE_CONFIG_FILE)//4
.withConfiguration(AutoConfigurations.of(BookService.class));//5
contextRunner
.run((context) -> {
assertThat(context)
.hasFailed();
assertThat(context.getStartupFailure())
.isNotNull();
assertThat(context.getStartupFailure().getMessage())
.contains("Some exception message");
assertThat(extractFailureCauseMessages(context))
.contains("Cause exception message");
});
}
private List<String> extractFailureCauseMessages(AssertableApplicationContext context) {
var failureCauseMessages = new ArrayList<String>();
var currentCause = context.getStartupFailure().getCause();
while (!Objects.isNull(currentCause)) {//7
failureCauseMessages.add(currentCause.getMessage());
currentCause = currentCause.getCause();
}
return failureCauseMessages;
}
使用 Junit5 中类似定义的示例进行解释 Spring 引导测试注释:
- 触发加载配置文件,例如
application.properties
或application.yml
- 当应用程序上下文失败时使用给定的日志级别记录
ConditionEvaluationReport
- 提供指定模拟 bean 的 class,即。我们在
BookService
中有@Autowired BookRepository
,在MockBeansTestConfiguration
中提供模拟BookRepository
。类似于测试 class 中的@Import({MockBeansTestConfiguration.class})
和 class 中的@TestConfiguration
以及普通 Junit5 中的模拟 bean Spring 启动测试 - 相当于
@TestPropertySource(properties = { "spring.config.location=" + POSITIVE_CASE_CONFIG_FILE})
- 触发 spring 给定 class 的自动配置,不是直接等效的,但它类似于使用
@ContextConfiguration(classes = {BookService.class})
或@SpringBootTest(classes = {BookService.class})
和@Import({BookService.class})
正常测试 - Assertions.class 来自 AssertJ 库,
Assertions.assertThat
应该有静态导入,但我想显示此方法的来源 Objects.isNull
应该有静态导入,但我想显示此方法的来源
MockBeansTestConfigurationclass:
@TestConfiguration
public class MockBeansTestConfiguration {
private static final Book SAMPLE_BOOK = Book.of(1L, "Stanisław Lem", "Solaris", "978-3-16-148410-0");
@Bean
public BookRepository mockBookRepository() {
var bookRepository = Mockito.mock(BookRepository.class);//1
Mockito.when(bookRepository.findByIsbn(SAMPLE_BOOK.getIsbn()))//2
.thenReturn(SAMPLE_BOOK);
return bookRepository;
}
}
备注:
1,2。应该有静态导入,但我想显示这个方法来自哪里