如何通过JUnit测试截取SLF4J(带回登录)日志?

kiz8lqtg  于 2022-11-11  发布在  其他
关注(0)|答案(9)|浏览(216)

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

laik7k3q

laik7k3q1#

Slf 4j API没有提供这样的方法,但是Logback提供了一个简单的解决方案。
您可以使用ListAppender:一个白盒logback appender,其中日志条目被添加到一个public List字段中,我们可以使用该字段来进行Assert。
这里有一个简单的例子。
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());
    }
}

您还可以将匹配器/Assert库用作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));
wlwcrazw

wlwcrazw2#

您可以创建自定义追加器

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-原因请参见注解部分。

8fsztsew

8fsztsew3#

您可以使用http://projects.lidalia.org.uk/slf4j-test/中的slf 4j-test,它将整个logback slf 4j实现替换为自己的slf 4j api实现以进行测试,并提供一个api来Assert日志事件。
例如:
第一个

3phpmpom

3phpmpom4#

使用JUnit 5

private ListAppender<ILoggingEvent> logWatcher;

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

注意:MyClass.class-应该是您的Prod类,您希望日志输出来自

* 用途:(AssertJ范例)*

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

* 销毁:*

为了获得更好的性能,建议使用“分离”:

@AfterEach
void teardown() {
  ((Logger) LoggerFactory.getLogger(MyClass.class)).detachAndStopAllAppenders();
}

* 导入:*

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

感谢:@davidxxx的答案。查看import ch.qos.logback...详细信息:https://stackoverflow.com/a/52229629/601844

wz3gfoph

wz3gfoph5#

一个简单的解决方案可以是用Mockito模拟附加器(例如)
第1001章:我的MyClass.java

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

第1001章:我的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;
        }));

    }

}

注意:我使用assertion而不是返回false,因为它使代码和(可能的)错误更容易阅读,但是如果你有多个验证,它就不起作用了。在这种情况下,你需要返回boolean来指示值是否如预期的那样。

j0pj023g

j0pj023g6#

尽管创建一个自定义的logback appender是一个很好的解决方案,但这只是第一步,最终您将开发/重新发明slf4j-test,如果您更进一步:spf4j-slf4j-test或其他我还不知道的框架。
您最终将需要考虑在内存中保存多少事件,当错误被记录(并且未被Assert)时单元测试失败,在测试失败时使调试日志可用,等等。
免责声明:我是spf 4j-slf 4j-test的作者,我写这个后端是为了能够更好地测试spf4j,这是一个很好的地方来看看如何使用spf 4j-slf 4j-test的例子。我实现的主要优势之一是减少了我的构建输出(这是特拉维斯的限制),同时仍然有我在失败发生时所需要的所有细节。

mbzjlibv

mbzjlibv7#

我推荐一个简单的、可重用的spy实现,它可以作为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()(或其他自定义方法)以检查记录的事件。

ibrsph3r

ibrsph3r8#

我在测试日志行时遇到问题,例如:记录器。错误(消息,异常)
http://projects.lidalia.org.uk/slf4j-test/中描述的解决方案也试图在异常上Assert,并且重新创建堆栈跟踪并不容易(在我看来毫无价值)。
我这样下定决心:

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 matchers库。

axkjgtzd

axkjgtzd9#

这是一个使用lambda的替代方案,它使日志捕获逻辑在测试中可重用(封装其实现),并且不需要@BeforeEach/@AfterEach(在一些建议的解决方案中,附加器没有分离,这可能导致内存泄漏)。

受试代码:

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

public class MyService {

    private static final Logger LOG = LoggerFactory.getLogger(MyService.class);

    public void doSomething(String someInput) {
        ...
        LOG.info("processing request with input {}", someInput);
        ...
    }
}

侦听器帮助程序:

package mypackage.util

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

import java.util.List;

public class LogInterceptor {

    public static List<ILoggingEvent> interceptLogs(Class<?> klass, Runnable runnable) {
        final Logger logger = (Logger) LoggerFactory.getLogger(klass);
        final ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
        listAppender.start();
        logger.addAppender(listAppender);
        try {
            runnable.run();
            return listAppender.list;
        } finally {
            logger.detachAppender(listAppender);
        }
    }
}

测试套件:

import static mypackage.util.LogInterceptor.interceptLogs;

public class MyServiceTest {

  private MyService myService; 
  ...

  @Test
  void doSomethingLogsLineWithTheGivenInput() {
        List<ILoggingEvent> logs = interceptLogs(
                myService.getClass(),
                () -> myService.doSomething("foo")
        );

        assertThat(logs).isNotEmpty();
        ILoggingEvent logEntry = logs.get(0);
        assertThat(logEntry.getFormattedMessage()).isEqualTo("Processing request with input foo");
        assertThat(logEntry.getLevel()).isEqualTo(Level.INFO);
  }

}

相关问题