使用 SpringBootTest 和 Autowired 注解时 LogCaptor 捕获失败

LogCaptor fails to capture when using SpringBootTest and Autowired annotation

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

使用特定设置我无法捕获日志,我不明白为什么会这样。我不确定我是否错过了 spring 启动测试配置的某些内容,或者是否还有其他内容。

所以让我们假设有一个 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,例如拥有一个构造函数并使用自动装配对其进行注释,并将 logcaptor 设置放在那里,但它们确实有效,但我不明白为什么上述特定情况会失败。有人知道可能是什么问题吗?

在下面尝试,它应该有效

@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 测试 - 它通过了

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

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

当您参加两个通过的 @SpringBootTest 测试中的任何一个并同时执行它们时,只要失败的测试没有先执行,您就可以使测试通过。那是因为 Spring 测试框架不会 bootstrap 一个新的上下文,而是重新使用第一个测试的上下文 运行。如果你用 @DirtiesContext(classMode = ClassMode.BEFORE_CLASS) 注释它的 class ,你可以让测试重新失败,因为在分配 class 变量 logcaptor 之后,将再次创建一个新的上下文。

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