是否可以InputStream
通过JUnit测试用例以某种方式拦截日志记录(SLF4J + logback)并获取(或其他可读的信息)...?
Answers:
您可以创建自定义附加程序
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
如果没有得到任何输出,请使用-请参阅注释部分以获取推理。
events
在每次测试执行后清除。
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));
Logger fooLogger = (Logger) LoggerFactory.getLogger(Foo.class);
。我正在使用LoggerFactory
oforg.slf4j.LoggerFactory
和Logger
ofch.qos.logback.classic.Logger
SLF4J
此解决方案,则将SLF4J: Class path contains multiple SLF4J bindings.
同时发出SLF4J和logback.classic信息,从而引发警告。
您可以从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();
}
}
slf4j-test
软件包的完整示例可以在以下位置找到:github.com/jaegertracing/jaeger-client-java/pull/378/files
一个简单的解决方案可能是使用Mockito模拟附加器(例如)
@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;
}));
}
}
注意:我使用断言而不是返回,false
因为它使代码和(可能)错误更易于阅读,但是如果您有多个验证,它将无法工作。在这种情况下,您需要返回以boolean
指示该值是否符合预期。
尽管创建自定义的logback附加程序是一个很好的解决方案,但这只是第一步,您最终将最终开发/重新发明slf4j-test,如果再进一步的话:spf4j-slf4j-test或其他我不喜欢的框架还不知道。
您最终将需要担心内存中会保留多少事件,记录错误(且未声明错误)时使单元测试失败,使测试失败时可以使用调试日志,等等。
免责声明:我是spf4j-slf4j-test的作者,我写了此后端以能够更好地测试spf4j,这是查看如何使用spf4j-slf4j-test的示例的好地方。我获得的主要优点之一是减少了构建输出(Travis限制了该输出),同时仍然保留了发生故障时所需的所有细节。
我建议一个简单的,可重用的间谍实现,该实现可以作为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()
(或其他自定义方法)以检查记录的事件。
import ch.qos.logback.classic.Logger;
,而不是import org.slf4j.LoggerFactory;
否则addAppender()
不可用。我花了一段时间才弄清楚这一点。
before()
并且after()
从未达到该规则,因此从未创建/附加该附加程序,并且触发了UnexpectedTestError。任何想法我在做什么错?规则是否需要放入特定包装中?另外,请为您的答案添加导入部分,因为某些对象/接口的名称不明确。
测试日志行时遇到问题: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匹配器库的优点。
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
ILoggingEvent
而不是LoggingEvent
。那对我有用。