Java 如何通过JUnit测试拦截SLF4J(带有logback)日志记录?
是否可以通过JUnit测试用例以某种方式拦截日志记录(SLF4J+logback)并获取Java 如何通过JUnit测试拦截SLF4J(带有logback)日志记录?,java,junit,slf4j,logback,Java,Junit,Slf4j,Logback,是否可以通过JUnit测试用例以某种方式拦截日志记录(SLF4J+logback)并获取InputStream(或其他可读的内容)…?您可以创建一个自定义附加程序 public class TestAppender extends AppenderBase<LoggingEvent> { static List<LoggingEvent> events = new ArrayList<>(); @Override protec
InputStream
(或其他可读的内容)…?您可以创建一个自定义附加程序
public class TestAppender extends AppenderBase<LoggingEvent> {
static List<LoggingEvent> events = new ArrayList<>();
@Override
protected void append(LoggingEvent e) {
events.add(e);
}
}
注意:如果您没有得到任何输出,请使用
ILOGINGEVENT
——有关推理,请参阅注释部分。您可以从中使用slf4j测试。
它将整个logback slf4j实现替换为用于测试的slf4j api实现,并提供了一个针对日志事件进行断言的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();
}
}
maven surefire插件
总qos.logback:经典logback
公共级Slf4jUser{
私有静态最终记录器Logger=LoggerFactory.getLogger(Slf4jUser.class);
public void aMethodThatLogs(){
logger.info(“你好,世界!”);
}
}
公共类Slf4jUserTest{
Slf4jUser Slf4jUser=新的Slf4jUser();
TestLogger logger=TestLoggerFactory.getTestLogger(Slf4jUser.class);
@试验
public void-amethodthatlogslogsaseexpected(){
slf4jUser.aMethodThatLogs();
断言(logger.getLoggingEvents())是(asList(info(“helloworld!”)));
}
@之后
公共空白清除记录者(){
TestLoggerFactory.clear();
}
}
我在测试日志行时遇到问题,例如:LOGGER.error(消息,异常)
中描述的解决方案也会尝试对异常进行断言,而重新创建stacktrace并不容易(而且在我看来毫无价值)
我这样决定:
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库的优点。Slf4j API不提供这种方法,但Logback提供了一个简单的解决方案 您可以使用:一个白盒logback appender,其中日志条目被添加到
公共列表中,我们可以使用该字段进行断言
下面是一个简单的例子
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");
}
}
最高级:
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());
}
}
尽管创建一个定制的logback appender是一个很好的解决方案,但这只是第一步,你最终会开发/重新发明,如果你走得更远一点:或者其他我还不知道的框架
您最终需要担心内存中保留了多少事件,当错误被记录(而不是断言)时单元测试失败,测试失败时调试日志可用,等等
免责声明:我是spf4j-slf4j-test的作者,我编写此后端是为了能够更好地进行测试,这是查看如何使用spf4j-slf4j-test的示例的好地方。我获得的一个主要优势是减少了构建输出(这受到Travis的限制),同时在发生故障时仍保留了所需的所有细节。一个简单的解决方案可以是使用Mockito模拟appender(例如)
MyClass.java
MyClassTest.java
import static org.hamcrest.matcherasert.assertThat;
导入静态org.hamcrest.Matchers.containssString;
导入静态org.hamcrest.Matchers.is;
导入静态org.mockito.mockito.verify;
@RunWith(MockitoJUnitRunner.class)
公共类MyClassTest{
@模拟专用附加器模拟附加器;
private MyClass sut=new MyClass();
@以前
公共作废设置(){
Logger Logger=(Logger)LoggerFactory.getLogger(MyClass.class.getName());
logger.addAppender(mockAppender);
}
@试验
公共无效应记录错误(){
sut.doSomething();
验证(mockAppender).doAppend(ArgumentMatchers.argThat)(参数->{
断言(argument.getMessage(),包含字符串(“我在上面!”);
断言(argument.getLevel(),是(Level.INFO));
返回true;
}));
}
}
注意:我使用的是断言,而不是返回false
,因为它使代码和(可能的)错误更易于阅读,但如果您有多个验证,它将不起作用。在这种情况下,您需要返回布尔值
,指示该值是否符合预期。我建议使用一个简单的、可重用的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;
}
}
调用log.getEvents()
(或其他自定义方法)检查记录的事件。使用JUnit5+AssertJ
私有ListAppender日志观察程序;
@之前
无效设置(){
this.logWatcher=新的ListAppender();
这是.logWatcher.start();
((Logger)LoggerFactory.getLogger(MyClass.class)).addAppender(this.logWatcher);
}
@试验
void myMethod_logs2Messages(){
...
int logSize=logWatcher.list.size();
assertThat(logWatcher.list.get(logSize-2.getFormattedMessage())包含(“预期的消息1”);
assertThat(logWatcher.list.get(logSize-1.getFormattedMessage())包含(“预期的消息2”);
}
归功于:@davidxxx的答案。有关导入ch.qos.logback…
详细信息,请参阅此文档:感谢您提供此备选答案!它看起来非常有用,我很可能在将来也会尝试这种方法!不幸的是,我已经接受了另一个同样正确的答案。在这里可以找到使用lidalia的slf4j测试包的完整示例:如果不使用Spring,此解决方案可以正常工作。如果您使用Spring,它将抛出类未找到异常(JoranConfigurator)。注意,如果您使用logback classic+slf4j,则需要使用ILOGingEvent
而不是LoggingEvent
。这对我来说很有用。@Evgeniy Dorofev您能演示一下如何配置logback test.xml吗?我想您需要在每次测试执行后清除事件。@hipokito您可以使用sample0.xml
中提到的[此处]()。别忘了将appender更改为您的implementaion@EvgeniyDorofeev你能帮我吗?非常感谢你!这正是我要找的!我得到了Logger-whoologger=(Logger)LoggerFactory.getLogger(Foo.class)的ClassCastException代码>。我正在使用日志
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));
@Slf4j
class MyClass {
public void doSomething() {
log.info("I'm on it!");
}
}
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;
}));
}
}
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();
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");
}