Java 如何通过 JUnit 测试拦截 SLF4J(带 logback)日志记录?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/29076981/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How to intercept SLF4J (with logback) logging via a JUnit test?
提问by carlspring
Is it possible to somehow intercept the logging (SLF4J + logback) and get an InputStream
(or something else that is readable) via a JUnit test case...?
是否有可能以某种方式拦截日志记录(SLF4J + logback)并InputStream
通过 JUnit 测试用例获取(或其他可读的东西)......?
采纳答案by Evgeniy Dorofeev
You can create a custom appender
您可以创建自定义 appender
public class TestAppender extends AppenderBase<LoggingEvent> {
static List<LoggingEvent> events = new ArrayList<>();
@Override
protected void append(LoggingEvent e) {
events.add(e);
}
}
and configure logback-test.xml to use it. Now we can check logging events from our test:
并配置 logback-test.xml 以使用它。现在我们可以从我们的测试中检查日志事件:
@Test
public void test() {
...
Assert.assertEquals(1, TestAppender.events.size());
...
}
回答by Oleg Majewski
You can use slf4j-test from http://projects.lidalia.org.uk/slf4j-test/. It replaces the entire logback slf4j implementation by it's own slf4j api implementation for tests and provides an api to assert against logging events.
您可以使用来自http://projects.lidalia.org.uk/slf4j-test/ 的slf4j-test 。它用自己的 slf4j api 实现替换了整个 logback slf4j 实现以进行测试,并提供了一个 api 来对日志事件进行断言。
example:
例子:
<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();
}
}
回答by daemon_nio
I had problems when testing logs line like: LOGGER.error(message, exception).
我在测试日志行时遇到了问题,例如:LOGGER.error(message, exception)。
The solution described in http://projects.lidalia.org.uk/slf4j-test/tries to assert as well on the exception and it is not easy (and in my opinion worthless) to recreate the stacktrace.
http://projects.lidalia.org.uk/slf4j-test/ 中描述的解决方案也尝试对异常进行断言,并且重新创建堆栈跟踪并不容易(在我看来毫无价值)。
I resolved in this way:
我是这样解决的:
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")
);
}
}
This has as well the advantage to not having depend on Hamcrest matcherslibrary.
这也具有不依赖Hamcrest 匹配器库的优势。
回答by davidxxx
The Slf4j API doesn't provide such a way but Logback provides a simple solution.
Slf4j API 没有提供这种方式,但 Logback 提供了一个简单的解决方案。
You can use ListAppender
: a whitebox logback appender where log entries are added in a public List
field that we could use to make our assertions.
您可以使用ListAppender
:一个白盒 logback appender,其中将日志条目添加public List
到我们可以用来进行断言的字段中。
Here is a simple example.
这是一个简单的例子。
Foo class :
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 class :
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());
}
}
You can also use Matcher/assertion libraries as AssertJ or Hamcrest.
您还可以使用匹配器/断言库作为 AssertJ 或 Hamcrest。
With AssertJ it would be :
使用 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));
回答by user2179737
Although creating a custom logback appender is a good solution, it is only the first step, you will eventually end up developing/reinventing slf4j-test, and if you go a bit further: spf4j-slf4j-testor other frameworks that I don't know of yet.
尽管创建自定义 logback appender 是一个很好的解决方案,但这只是第一步,您最终将最终开发/重新发明slf4j-test,如果您更进一步:spf4j-slf4j-test或其他我不知道的框架还不知道。
You will eventually need to worry about how many events you keep in memory, fail unit tests when a error is logged (and not asserted), make debug logs available on test failure, etc...
您最终将需要担心您在内存中保留了多少事件,在记录错误(而不是断言)时使单元测试失败,在测试失败时使调试日志可用,等等......
Disclaimer: I am the author of spf4j-slf4j-test, I wrote this backend to be able to better test spf4j, which is a good place to look at for examples on how to use spf4j-slf4j-test. One of the main advantages I achieved was reducing my build output (which is limited with Travis), while still having all the detail I need when failure happens.
免责声明:我是 spf4j-slf4j-test 的作者,我写这个后端是为了能够更好地测试spf4j,这是查看如何使用 spf4j-slf4j-test 的示例的好地方。我获得的主要优势之一是减少了我的构建输出(这在 Travis 中受到限制),同时在发生故障时仍然拥有我需要的所有细节。
回答by snovelli
A simple solution could be to mock the appender with Mockito (for example)
一个简单的解决方案可能是用 Mockito 模拟附加程序(例如)
MyClass.java
我的类
@Slf4j
class MyClass {
public void doSomething() {
log.info("I'm on it!");
}
}
MyClassTest.java
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;
}));
}
}
NOTE: I'm using assertion rather than returning false
as it makes code and (possible) error easier to read, but it won't work if you have multiple verifications. In that case you need to return boolean
indicating if the value is as expected.
注意:我使用断言而不是返回,false
因为它使代码和(可能的)错误更易于阅读,但如果您有多次验证,它将不起作用。在这种情况下,您需要返回boolean
指示该值是否符合预期。
回答by oberlies
I would recommend a simple, reusable spy implementation that can be included in a test as JUnit rule:
我会推荐一个简单的、可重用的间谍实现,它可以作为 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;
}
}
In your test, you'd activate the spy in the following way:
在您的测试中,您将通过以下方式激活间谍:
@Rule
public LogSpy log = new LogSpy();
Call log.getEvents()
(or other, custom methods) to check the logged events.
调用log.getEvents()
(或其他自定义方法)以检查记录的事件。