如何通过JUnit测试拦截SLF4J(带回送)日志记录?


76

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

Answers:


40

您可以创建自定义附加程序

public class TestAppender extends AppenderBase<LoggingEvent> {
    static List<LoggingEvent> events = new ArrayList<>();
    
    @Override
    protected void append(LoggingEvent e) {
        events.add(e);
    }
}

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

@Test
public void test() {
    ...
    Assert.assertEquals(1, TestAppender.events.size());
    ...
}

注意:ILoggingEvent如果没有得到任何输出,请使用-请参阅注释部分以获取推理。


16
请注意,如果您使用的是logback classic + slf4j,则需要使用ILoggingEvent而不是LoggingEvent。那对我有用。
etech

6
@Evgeniy Dorofeev您能否显示如何配置logback-test.xml?
hipokito

1
我认为您需要events在每次测试执行后清除。
Andrii Karaivanskyi

2
@hipokito您可以在中使用[此处](logback.qos.ch/manual/configuration.html)中提到的那个sample0.xml。别忘了更改您的实现的附加程序
encoding_idiot

@EvgeniyDorofeev可以帮我吗?stackoverflow.com/questions/48551083/...
巴维亚阿罗拉

73

Slf4j API没有提供这种方法,但是Logback提供了一种简单的解决方案。

您可以使用ListAppender:whitebox logback追加器,在其中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");
    }
}

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());
    }
}

您还可以将Matcher / assertion库用作AssertJ或Hamcrest。

使用AssertJ,它将是:

import org.assertj.core.api.Assertions;

Assertions.assertThat(listAppender.list)
          .extracting(ILoggingEvent::getFormattedMessage, ILoggingEvent::getLevel)
          .containsExactly(Tuple.tuple("start", Level.INFO), Tuple.tuple("finish", Level.INFO));

2
非常感谢!这正是我想要的!
奥利

5
我正在获取ClassCastException Logger fooLogger = (Logger) LoggerFactory.getLogger(Foo.class);。我正在使用LoggerFactoryoforg.slf4j.LoggerFactoryLoggerofch.qos.logback.classic.Logger
Hiren,

@Hiren确切的错误消息是什么?
davidxxx '19

7
需要注意的重要一点是,如果日志包含参数值,则应该使用ILoggingEvent :: getFormattedMessage而不是ILoggingEvent :: getMessage。否则,您的断言将失败,因为该值将丢失。
罗伯特·梅森

4
如果您使用的是SLF4J此解决方案,则将SLF4J: Class path contains multiple SLF4J bindings.同时发出SLF4J和logback.classic信息,从而引发警告。
Ghilteras

16

您可以从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();
    }
}

感谢您选择替代答案!它看起来非常有用,将来我也很可能会尝试这种方法!不幸的是,我已经接受了另一个正确的答案。
carlspring

使用lidaliaslf4j-test软件包的完整示例可以在以下位置找到:github.com/jaegertracing/jaeger-client-java/pull/378/files
Debosmit Ray

1
如果您不使用Spring,则此解决方案效果很好。如果使用Spring,它将抛出未找到的类异常(JoranConfigurator)。
耶稣H

7

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

MyClass.java

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

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;
        }));

    }

}

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


如果我使用的是像Slf4j这样的lombok.extern.slf4j注释,这行得通吗?如果它甚至不是我所讨论的对象,您如何嘲笑或监视记录器?即log.error只是通过提供我的课注释SLF4J使用...
ennth

@ennth应该起作用,因为您正在向静态方法LoggerFactory.getLogger()。addAppender(mockAppender)注入模拟。使用Lombok创建记录器时,它们的工作方式相同
-snovelli

2
有同样的不工作的问题。Logger和LoggerFactory类的“导入”是什么?为什么列出静态进口而没有列出静态进口?
德克·舒马赫

5

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

您最终将需要担心内存中会保留多少事件,记录错误(且未声明错误)时使单元测试失败,使测试失败时可以使用调试日志,等等。

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


4

我建议一个简单的,可重用的间谍实现,该实现可以作为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;
    }
}

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

@Rule
public LogSpy log = new LogSpy();

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


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

对我不起作用。似乎该规则未正确应用:在调试时我发现before()并且after()从未达到该规则,因此从未创建/附加该附加程序,并且触发了UnexpectedTestError。任何想法我在做什么错?规则是否需要放入特定包装中?另外,请为您的答案添加导入部分,因为某些对象/接口的名称不明确。
Philzen '20

2

测试日志行时遇到问题:LOGGER.error(message,exception)

http://projects.lidalia.org.uk/slf4j-test/中描述的解决方案也尝试对异常进行断言,并且重新创建堆栈跟踪并不容易(我认为毫无价值)。

我以这种方式解决:

import org.junit.Test;
import org.slf4j.Logger;
import uk.org.lidalia.slf4jext.LoggerFactory;
import uk.org.lidalia.slf4jtest.TestLogger;
import uk.org.lidalia.slf4jtest.TestLoggerFactory;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.groups.Tuple.tuple;
import static uk.org.lidalia.slf4jext.Level.ERROR;
import static uk.org.lidalia.slf4jext.Level.INFO;


public class Slf4jLoggerTest {

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


    private void methodUnderTestInSomeClassInProductionCode() {
        LOGGER.info("info message");
        LOGGER.error("error message");
        LOGGER.error("error message with exception", new RuntimeException("this part is not tested"));
    }





    private static final TestLogger TEST_LOGGER = TestLoggerFactory.getTestLogger(Slf4jLoggerTest.class);

    @Test
    public void testForMethod() throws Exception {
        // when
        methodUnderTestInSomeClassInProductionCode();

        // then
        assertThat(TEST_LOGGER.getLoggingEvents()).extracting("level", "message").contains(
                tuple(INFO, "info message"),
                tuple(ERROR, "error message"),
                tuple(ERROR, "error message with exception")
        );
    }

}

这也具有不依赖Hamcrest匹配器库的优点。


2

使用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");
}

致谢:@davidxxx的答案。看到它了解import ch.qos.logback...更多信息:https : //stackoverflow.com/a/52229629/601844

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.