使用 SpringBootTest 和 Autowired 注解时 LogCaptor 无法捕获

Posted

技术标签:

【中文标题】使用 SpringBootTest 和 Autowired 注解时 LogCaptor 无法捕获【英文标题】:LogCaptor fails to capture when using SpringBootTest and Autowired annotation 【发布时间】:2022-01-12 11:06:49 【问题描述】:

在使用注释 SpringBootTest 和自动装配服务时,我在捕获日志的集成测试中有一个奇怪的行为。我使用LogCaptor 来捕获日志。

通过特定设置,我无法捕获日志,而且我无法理解为什么会发生这种情况。我不确定我是否错过了 Spring Boot 测试配置中的某些内容,或者是否是其他内容。

所以我们假设有一个 FooService:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

@Service
public class FooService 

    private static final Logger LOGGER = LoggerFactory.getLogger(FooService.class);

    public String hello() 
        LOGGER.error("Expected error message");
        return "hello";
    


以及集成测试:

这个失败了

import nl.altindag.log.LogCaptor;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
public class AppIT 

    private static final LogCaptor logcaptor = LogCaptor.forClass(FooService.class);

    @Autowired
    private FooService fooService;

    @Test
    void testMethod() 
        fooService.hello();
        assertThat(logcaptor.getErrorLogs()).containsExactly("Expected error message");
    


上面的测试会失败,但是当我不使用 SpringBootTest 注释并且只是在测试方法中初始化 FooService 时它通过了:

这些都是通过

import nl.altindag.log.LogCaptor;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class AppIT 

    private static final LogCaptor logcaptor = LogCaptor.forClass(FooService.class);

    @Test
    void testMethod() 
        new FooService().hello();
        assertThat(logcaptor.getErrorLogs()).containsExactly("Expected error message");
    


import nl.altindag.log.LogCaptor;
import nl.altindag.server.service.FooService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.PostConstruct;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
public class AppIT 

    private FooService fooService;
    private LogCaptor logcaptor;

    @Autowired
    AppIT(FooService fooService) 
        this.fooService = fooService;
    

    @PostConstruct
    void init() 
        this.logcaptor = LogCaptor.forClass(FooService.class);
    

    @Test
    void testMethod() 
        fooService.hello();
        assertThat(logcaptor.getErrorLogs()).containsExactly("Expected error message");
    


import nl.altindag.log.LogCaptor;
import nl.altindag.server.service.FooService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
public class AppIT 

    private FooService fooService;
    private LogCaptor logcaptor;

    @Autowired
    AppIT(FooService fooService) 
        this.fooService = fooService;
        this.logcaptor = LogCaptor.forClass(FooService.class);
    

    @Test
    void testMethod() 
        fooService.hello();
        assertThat(logcaptor.getErrorLogs()).containsExactly("Expected error message");
    


已使用以下库:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.6.1</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot</artifactId>
    <version>2.6.1</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <version>2.6.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.8.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.8.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <version>3.21.0</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>io.github.hakky54</groupId>
    <artifactId>logcaptor</artifactId>
    <version>2.7.2</version>
    <scope>test</scope>
</dependency>

我曾尝试在测试和其他设置中使用 postconstruct,例如拥有一个构造函数并使用 autowired 对其进行注释并将 logcaptor 设置放在那里确实有效,但我不明白为什么上述具体情况会失败。有人知道可能是什么问题吗?

【问题讨论】:

【参考方案1】:

失败测试的问题是类变量logcaptor是在创建测试应用上下文之前创建的。在测试应用上下文的引导过程中,Spring Boot 会完全从头开始配置 logback。这会导致 LogCaptor::forClass 中添加的 appender 丢失。

在成功的@SpringBootTest 测试中,附加程序是在上下文生命周期的后期添加的。

只要不首先执行失败的测试,您就可以通过两个通过@SpringBootTest 测试中的任何一个并一起执行它们,从而使测试通过。这是因为 Spring 测试框架不会引导新的上下文,而是重用第一个运行的测试的上下文。如果你用@DirtiesContext(classMode = ClassMode.BEFORE_CLASS) 注释它的类,你可以让测试恢复失败,因为在分配类变量logcaptor 之后将再次创建一个新的上下文。

恐怕对此无能为力。 Spring Boot 团队在 github 上的这条评论是关于类似的问题:https://github.com/spring-projects/spring-boot/issues/19323#issuecomment-563166919

【讨论】:

很好的答案,我同意你的看法。【参考方案2】:

试试下面,它应该可以工作

@SpringBootTest
public class AppIT 

    private static LogCaptor logcaptor = LogCaptor.forClass(FooService.class);

    @Autowired
    private FooService fooService;

    @BeforeEach
    void beforeEach() 
        logcaptor = LogCaptor.forClass(FooService.class);
        logcaptor.clearLogs();
    

    @Test
    void testMethod() 
        fooService.hello();
        assertThat(logcaptor.getErrorLogs()).containsExactly("Expected error message");
    


在 eclipse 上运行测试 - 它通过了

【讨论】:

感谢您的回答,但我很好奇为什么我的示例中的特定设置失败了,所以这是我的主要问题

以上是关于使用 SpringBootTest 和 Autowired 注解时 LogCaptor 无法捕获的主要内容,如果未能解决你的问题,请参考以下文章

调试器、@SpringBootTest 和 Gradle

SpringBootTest 和Testng组成测试套件

@SpringBootTest 需要数据库连接?

如何使用@SpringBootTest 验证作业是不是运行了另一个作业

使用 @SpringBootTest 测试时出现 HttpMessageNotWritableException

SpringBootTest - 如何在测试级别断言上下文不加载和更改属性?