测试 spring 应用程序上下文无法启动的最佳方法是啥?

Posted

技术标签:

【中文标题】测试 spring 应用程序上下文无法启动的最佳方法是啥?【英文标题】:What is the best way to test that a spring application context fails to start?测试 spring 应用程序上下文无法启动的最佳方法是什么? 【发布时间】:2015-10-19 23:40:09 【问题描述】:

我使用 spring-boot-starter-web 和 spring-boot-starter-test。

假设我有一个用于绑定配置属性的类:

@ConfigurationProperties(prefix = "dummy")
public class DummyProperties 

    @URL
    private String url;

    // getter, setter ...


现在我想测试我的 bean 验证是否正确。如果未设置属性 dummy.value 或它包含无效的 URL,则上下文应该无法启动(带有特定的错误消息)。如果属性包含有效的 URL,则上下文应该开始。 (测试会显示@NotNull 缺失。)

一个测试类应该是这样的:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = MyApplication.class)
@IntegrationTest( "dummy.url=123:456" )
public class InvalidUrlTest 
    // my test code

此测试将失败,因为提供的属性无效。告诉 Spring/JUnit 的最佳方式是什么:“是的,这个错误是意料之中的”。在普通的 JUnit 测试中,我会使用 ExpectedException。

【问题讨论】:

【参考方案1】:

测试Spring应用上下文的最好方法是使用ApplicationContextRunner

在 Spring Boot 参考文档中有描述: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 Boot Test Annotations 中类似定义的示例说明:

    触发加载配置文件,如application.propertiesapplication.yml 在应用程序上下文失败时使用给定的日志级别记录 ConditionEvaluationReport 提供指定模拟 bean 的类,即。我们的BookService 中有@Autowired BookRepository,我们在MockBeansTestConfiguration 中提供模拟BookRepository。类似于测试类中的@Import(MockBeansTestConfiguration.class) 和普通Junit5 Spring Boot 测试中带有模拟bean 的类中的@TestConfiguration 相当于@TestPropertySource(properties = "spring.config.location=" + POSITIVE_CASE_CONFIG_FILE) 触发给定类的弹簧自动配置,不是直接等效的,但类似于在正常测试中使用@ContextConfiguration(classes = BookService.class)@SpringBootTest(classes = BookService.class)@Import(BookService.class)一起使用 AssertJ 库中的 Assertions.class,Assertions.assertThat 应该有静态导入,但我想说明这个方法的来源 Objects.isNull 应该有静态导入,但我想说明这个方法的来源

MockBeansTestConfiguration 类:

@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。应该有静态导入,但我想说明这个方法是从哪里来的

【讨论】:

在 5 年以上的 Spring 经验之后,ApplicationContextRunner 是我最喜欢的测试类之一。这对我描述的用例非常有帮助。 @RolandWeisleder 我完全同意,我有同样多年的引导经验,我不敢相信我以前从未见过这门课。【参考方案2】:

为什么首先要进行集成测试?你为什么要为此启动一个完整的 Spring Boot 应用程序?

对我来说,这看起来像是单元测试。话虽如此,您有多种选择:

不要添加@IntegrationTest,Spring Boot 将不会启动 Web 服务器(使用 @PropertySource 将值传递给您的测试,但将无效值传递给整个测试类感觉不对)李> 您可以使用spring.main.web-environment=false 禁用网络服务器(但鉴于上述观点,这很愚蠢) 编写一个单元测试来处理你的DummyProperties。您甚至不需要为此启动 Spring Boot 应用程序。看our own test suite

我肯定会选择最后一个。也许您有充分的理由为此进行集成测试?

【讨论】:

【参考方案3】:

我认为最简单的方法是:

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

【讨论】:

好主意,但如果没有抛出预期的异常,这将不起作用。由于 web-starter-pom,主要方法将启动一个网络服务器。测试将等待服务器关闭。 如果您担心无限等待(是的,这总是一个好主意),您始终可以应用超时(通过规则或@Timeout 注释)。查看更新版本。

以上是关于测试 spring 应用程序上下文无法启动的最佳方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

使用 Testcontainer 进行 Spring 集成测试 - 数据库在应用程序之后启动

无法在使用 @ExtendWith(SpringExtension.class) 运行的 Spring Boot 单元测试中注入应用程序上下文

如何在 Spring 中测试 @Configuration 类而不启动整个上下文?

带有休眠应用程序的 Spring Boot 无法启动

Spring Boot Test 无法使用 JUnit 5 为自定义集成测试源集加载应用程序上下文

如何使用 Spring 测试模拟的 JNDI 数据源?