如何通过JUnit测试拦截SLF4J日志记录?

car*_*ing 49 java junit logback slf4j

是否有可能以某种方式拦截日志记录(SLF4J + logback)并InputStream通过JUnit测试用例获取(或其他可读的内容)......?

Evg*_*eev 32

您可以创建自定义appender

public class TestAppender extends AppenderBase<LoggingEvent> {
    static List<LoggingEvent> events = new ArrayList<>();

    @Override
    protected void append(LoggingEvent e) {
        events.add(e);
    }
}
Run Code Online (Sandbox Code Playgroud)

并配置logback-test.xml以使用它.现在我们可以从测试中检查记录事件:

@Test
public void test() {
    ...
    Assert.assertEquals(1, TestAppender.events.size());
    ...
}
Run Code Online (Sandbox Code Playgroud)

  • 注意,如果你使用logback classic + slf4j,你需要使用`ILoggingEvent`而不是`LoggingEvent`.这对我有用. (11认同)
  • @Evgeniy Dorofeev 你能展示一下如何配置 logback-test.xml 吗? (6认同)
  • 我假设您需要在每次测试执行后清除“事件”。 (2认同)
  • @hipokito 您可以使用 `sample0.xml` 中提到的 [here] (https://logback.qos.ch/manual/configuration.html)。不要忘记将附加程序更改为您的实现 (2认同)

dav*_*xxx 24

Slf4j API不提供这种方式,但Logback提供了一个简单的解决方案.

您可以使用ListAppender:whitebox logback appender,其中日志条目添加public List到我们可用于进行断言的字段中.

这是一个简单的例子.

Foo类:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Foo {

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

    public void doThat() {
        logger.info("start");
        //...
        logger.info("finish");
    }
}
Run Code Online (Sandbox Code Playgroud)

FooTest类:

import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;

public class FooTest {

    @Test
    void doThat() throws Exception {
        // get Logback Logger 
        Logger fooLogger = (Logger) LoggerFactory.getLogger(Foo.class);

        // create and start a ListAppender
        ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
        listAppender.start();

        // add the appender to the logger
        fooLogger.addAppender(listAppender);

        // call method under test
        Foo foo = new Foo();
        foo.doThat();

        // JUnit assertions
        List<ILoggingEvent> logsList = listAppender.list;
        assertEquals("start", logsList.get(0)
                                      .getMessage());
        assertEquals(Level.INFO, logsList.get(0)
                                         .getLevel());

        assertEquals("finish", logsList.get(1)
                                       .getMessage());
        assertEquals(Level.INFO, logsList.get(1)
                                         .getLevel());
    }
}
Run Code Online (Sandbox Code Playgroud)

您还可以使用Matcher /断言库作为AssertJ或Hamcrest.

使用AssertJ,它将是:

import org.assertj.core.api.Assertions;

Assertions.assertThat(listAppender.list)
          .extracting(ILoggingEvent::getMessage, ILoggingEvent::getLevel)
          .containsExactly(Tuple.tuple("start", Level.INFO), Tuple.tuple("finish", Level.INFO));
Run Code Online (Sandbox Code Playgroud)

  • 需要注意的是,如果您的日志包含参数值,则应使用 ILoggingEvent::getFormattedMessage,而不是 ILoggingEvent::getMessage。否则,您的断言将失败,因为该值将丢失。 (7认同)
  • 如果您使用“SLF4J”,此解决方案最终将引发“SLF4J:类路径包含多个 SLF4J 绑定。”警告,因为您同时拥有 SLF4J 和 logback.classic (5认同)
  • 我正在为Logger fooLogger =(Logger)LoggerFactory.getLogger(Foo.class);获得ClassCastException。我正在使用org.slf4j.LoggerFactory的LoggerFactory和ch.qos.logback.classic.Logger的Logger。 (3认同)
  • 非常感谢!这正是我要找的! (2认同)

Ole*_*ski 12

您可以使用http://projects.lidalia.org.uk/slf4j-test/中的 slf4j-test .它通过它自己的slf4j api实现来替换整个logback slf4j实现,并提供一个api来对记录事件进行断言.

例:

<build>
  <plugins>
    <plugin>
      <artifactId>maven-surefire-plugin</artifactId>
      <configuration>
        <classpathDependencyExcludes>
          <classpathDependencyExcludes>ch.qos.logback:logback-classic</classpathDependencyExcludes>
        </classpathDependencyExcludes>
      </configuration>
    </plugin>
  </plugins>
</build>

public class Slf4jUser {

    private static final Logger logger = LoggerFactory.getLogger(Slf4jUser.class);

    public void aMethodThatLogs() {
        logger.info("Hello World!");
    }
}

public class Slf4jUserTest {

    Slf4jUser slf4jUser = new Slf4jUser();
    TestLogger logger = TestLoggerFactory.getTestLogger(Slf4jUser.class);

    @Test
    public void aMethodThatLogsLogsAsExpected() {
        slf4jUser.aMethodThatLogs();

        assertThat(logger.getLoggingEvents(), is(asList(info("Hello World!"))));
    }

    @After
    public void clearLoggers() {
        TestLoggerFactory.clear();
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 如果您不使用 Spring,此解决方案可以正常工作。如果使用Spring,它会抛出类未找到异常(JoranConfigurator)。 (2认同)

epo*_*pox 12

使用 JUnit5 + AssertJ

private ListAppender<ILoggingEvent> logWatcher;

@BeforeEach
void setup() {
  this.logWatcher = new ListAppender<>();
  this.logWatcher.start();
  ((Logger) LoggerFactory.getLogger(MyClass.class)).addAppender(this.logWatcher);
}


@Test
void myMethod_logs2Messages() {

  ...
  int logSize = logWatcher.list.size();
  assertThat(logWatcher.list.get(logSize - 2).getFormattedMessage()).contains("EXPECTED MSG 1");
  assertThat(logWatcher.list.get(logSize - 1).getFormattedMessage()).contains("EXPECTED MSG 2");
}
Run Code Online (Sandbox Code Playgroud)

归功于:@davidxxx 的回答。看到它import ch.qos.logback...的细节:/sf/answers/3656074061/

  • 看起来很有希望。但随后我收到“类路径包含多个 SLF4J 提供程序”警告,以及“java.lang.ClassCastException: org.slf4j.simple.SimpleLogger 无法转换为 ch.qos.logback.classic.Logger”错误。我尝试运行 `mvn dependency:tree` 来禁用 slf4j 提供程序,但失败了。:-/ (2认同)

sno*_*lli 9

一个简单的解决方案可能是用 Mockito 模拟附加程序(例如)

我的类

@Slf4j
class MyClass {
    public void doSomething() {
        log.info("I'm on it!");
    }
}
Run Code Online (Sandbox Code Playgroud)

MyClassTest.java

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.mockito.Mockito.verify;         

@RunWith(MockitoJUnitRunner.class)
public class MyClassTest {    

    @Mock private Appender<ILoggingEvent> mockAppender;
    private MyClass sut = new MyClass();    

    @Before
    public void setUp() {
        Logger logger = (Logger) LoggerFactory.getLogger(MyClass.class.getName());
        logger.addAppender(mockAppender);
    }

    @Test
    public void shouldLogInCaseOfError() {
        sut.doSomething();

        verify(mockAppender).doAppend(ArgumentMatchers.argThat(argument -> {
            assertThat(argument.getMessage(), containsString("I'm on it!"));
            assertThat(argument.getLevel(), is(Level.INFO));
            return true;
        }));

    }

}
Run Code Online (Sandbox Code Playgroud)

注意:我使用断言而不是返回,false因为它使代码和(可能的)错误更易于阅读,但如果您有多次验证,它将不起作用。在这种情况下,您需要返回boolean指示该值是否符合预期。

  • 有同样的不工作问题。Logger 和 LoggerFactory 类的“导入”是什么?为什么列出了静态导入,而没有列出其他导入? (3认同)
  • @DirkSchumacher我也有同样的困惑,它适用于以下导入:`import org.slf4j.LoggerFactory;``import ch.qos.logback.classic.Level;``import ch.qos.logback.classic.Logger;` `导入 ch.qos.logback.classic.spi.ILoggingEvent;` `导入 ch.qos.logback.core.Appender;` (3认同)

use*_*737 6

尽管创建自定义 logback appender 是一个很好的解决方案,但这只是第一步,您最终将最终开发/重新发明slf4j-test,如果您更进一步:spf4j-slf4j-test或其他我不知道的框架还不知道。

您最终将需要担心您在内存中保留了多少事件,在记录错误(而不是断言)时使单元测试失败,在测试失败时使调试日志可用,等等......

免责声明:我是 spf4j-slf4j-test 的作者,我写这个后端是为了能够更好地测试spf4j,这是查看如何使用 spf4j-slf4j-test 的示例的好地方。我获得的主要优势之一是减少了我的构建输出(这在 Travis 中受到限制),同时在发生故障时仍然拥有我需要的所有细节。


obe*_*ies 6

我会推荐一个简单的、可重用的间谍实现,它可以作为 JUnit 规则包含在测试中:

public final class LogSpy extends ExternalResource {

    private Logger logger;
    private ListAppender<ILoggingEvent> appender;

    @Override
    protected void before() {
        appender = new ListAppender<>();
        logger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); // cast from facade (SLF4J) to implementation class (logback)
        logger.addAppender(appender);
        appender.start();
    }

    @Override
    protected void after() {
        logger.detachAppender(appender);
    }

    public List<ILoggingEvent> getEvents() {
        if (appender == null) {
            throw new UnexpectedTestError("LogSpy needs to be annotated with @Rule");
        }
        return appender.list;
    }
}
Run Code Online (Sandbox Code Playgroud)

在您的测试中,您将通过以下方式激活间谍:

@Rule
public LogSpy log = new LogSpy();
Run Code Online (Sandbox Code Playgroud)

调用log.getEvents()(或其他自定义方法)以检查记录的事件。

  • 为了使其工作,您需要“import ch.qos.logback.classic.Logger;”而不是“import org.slf4j.LoggerFactory;”,否则“addAppender()”不可用。我花了一段时间才弄清楚这一点。 (2认同)