是否可以通过JUnit测试用例以某种方式截取日志记录(SLF4J + logback)并获得InputStream(或其他可读的内容)...?
InputStream
laik7k3q1#
Slf 4j API没有提供这样的方法,但是Logback提供了一个简单的解决方案。您可以使用ListAppender:一个白盒logback appender,其中日志条目被添加到一个public List字段中,我们可以使用该字段来进行Assert。这里有一个简单的例子。Foo类:
ListAppender
public List
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));
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-原因请参见注解部分。
ILoggingEvent
8fsztsew3#
您可以使用http://projects.lidalia.org.uk/slf4j-test/中的slf 4j-test,它将整个logback slf 4j实现替换为自己的slf 4j api实现以进行测试,并提供一个api来Assert日志事件。例如:第一个
3phpmpom4#
private ListAppender<ILoggingEvent> logWatcher; @BeforeEach void setup() { logWatcher = new ListAppender<>(); logWatcher.start(); ((Logger) LoggerFactory.getLogger(MyClass.class)).addAppender(logWatcher); }
注意:MyClass.class-应该是您的Prod类,您希望日志输出来自
@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
import ch.qos.logback...
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来指示值是否如预期的那样。
false
boolean
j0pj023g6#
尽管创建一个自定义的logback appender是一个很好的解决方案,但这只是第一步,最终您将开发/重新发明slf4j-test,如果您更进一步:spf4j-slf4j-test或其他我还不知道的框架。您最终将需要考虑在内存中保存多少事件,当错误被记录(并且未被Assert)时单元测试失败,在测试失败时使调试日志可用,等等。免责声明:我是spf 4j-slf 4j-test的作者,我写这个后端是为了能够更好地测试spf4j,这是一个很好的地方来看看如何使用spf 4j-slf 4j-test的例子。我实现的主要优势之一是减少了我的构建输出(这是特拉维斯的限制),同时仍然有我在失败发生时所需要的所有细节。
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()(或其他自定义方法)以检查记录的事件。
log.getEvents()
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库。
axkjgtzd9#
这是一个使用lambda的替代方案,它使日志捕获逻辑在测试中可重用(封装其实现),并且不需要@BeforeEach/@AfterEach(在一些建议的解决方案中,附加器没有分离,这可能导致内存泄漏)。
@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); } }
9条答案
按热度按时间laik7k3q1#
Slf 4j API没有提供这样的方法,但是Logback提供了一个简单的解决方案。
您可以使用
ListAppender
:一个白盒logback appender,其中日志条目被添加到一个public List
字段中,我们可以使用该字段来进行Assert。这里有一个简单的例子。
Foo类:
FooTest类:
您还可以将匹配器/Assert库用作AssertJ或Hamcrest。
对于AssertJ,它将是:
wlwcrazw2#
您可以创建自定义追加器
并配置logback-test.xml来使用它。现在我们可以检查测试中的日志事件:
注:如果未获得任何输出,请使用
ILoggingEvent
-原因请参见注解部分。8fsztsew3#
您可以使用http://projects.lidalia.org.uk/slf4j-test/中的slf 4j-test,它将整个logback slf 4j实现替换为自己的slf 4j api实现以进行测试,并提供一个api来Assert日志事件。
例如:
第一个
3phpmpom4#
使用JUnit 5
注意:MyClass.class-应该是您的Prod类,您希望日志输出来自
* 用途:(AssertJ范例)*
* 销毁:*
为了获得更好的性能,建议使用“分离”:
* 导入:*
感谢:@davidxxx的答案。查看
import ch.qos.logback...
详细信息:https://stackoverflow.com/a/52229629/601844wz3gfoph5#
一个简单的解决方案可以是用Mockito模拟附加器(例如)
第1001章:我的MyClass.java
第1001章:我的MyClassTest.java
注意:我使用assertion而不是返回
false
,因为它使代码和(可能的)错误更容易阅读,但是如果你有多个验证,它就不起作用了。在这种情况下,你需要返回boolean
来指示值是否如预期的那样。j0pj023g6#
尽管创建一个自定义的logback appender是一个很好的解决方案,但这只是第一步,最终您将开发/重新发明slf4j-test,如果您更进一步:spf4j-slf4j-test或其他我还不知道的框架。
您最终将需要考虑在内存中保存多少事件,当错误被记录(并且未被Assert)时单元测试失败,在测试失败时使调试日志可用,等等。
免责声明:我是spf 4j-slf 4j-test的作者,我写这个后端是为了能够更好地测试spf4j,这是一个很好的地方来看看如何使用spf 4j-slf 4j-test的例子。我实现的主要优势之一是减少了我的构建输出(这是特拉维斯的限制),同时仍然有我在失败发生时所需要的所有细节。
mbzjlibv7#
我推荐一个简单的、可重用的spy实现,它可以作为JUnit规则包含在测试中:
在您的测试中,您将通过以下方式激活间谍程序:
调用
log.getEvents()
(或其他自定义方法)以检查记录的事件。ibrsph3r8#
我在测试日志行时遇到问题,例如:记录器。错误(消息,异常)。
http://projects.lidalia.org.uk/slf4j-test/中描述的解决方案也试图在异常上Assert,并且重新创建堆栈跟踪并不容易(在我看来毫无价值)。
我这样下定决心:
这也有好处,不必依赖Hamcrest matchers库。
axkjgtzd9#
这是一个使用lambda的替代方案,它使日志捕获逻辑在测试中可重用(封装其实现),并且不需要
@BeforeEach
/@AfterEach
(在一些建议的解决方案中,附加器没有分离,这可能导致内存泄漏)。受试代码:
侦听器帮助程序:
测试套件: