有没有办法通过JUnit测试用例截取日志记录(SLF4J + logback),并获取InputStream
(或其他可读的内容)?
Slf4j API没有提供这样的方法,但Logback提供了一个简单的解决方案。
您可以使用 ListAppender
:一个白盒子 logback appender,其中日志条目添加在一个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/断言库,例如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));
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
而不是 LoggingEvent
。 这对我有效。 - etechprivate ListAppender<ILoggingEvent> logWatcher;
@BeforeEach
void setup() {
logWatcher = new ListAppender<>();
logWatcher.start();
((Logger) LoggerFactory.getLogger(MyClass.class)).addAppender(logWatcher);
}
注意:MyClass.class应该替换为您期望从中获取日志输出的产品类。
@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");
}
为更好的性能,建议使用分离(detach):
@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://dev59.com/dF4b5IYBdhLWcg3wdxnG#52229629
import ch.qos.logback.core.read.ListAppender;
- undefined<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
包的完整示例:https://github.com/jaegertracing/jaeger-client-java/pull/378/files - Debosmit Ray一个简单的解决方案是使用Mockito模拟(例如)appender。
@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
。
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.Appender;
- Egemenpublic 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()
(或其他自定义方法)检查已记录的事件。
ch.qos.logback.classic.Logger
而不是 org.slf4j.LoggerFactory
,否则 addAppender()
方法将无法使用。我花了一段时间才弄清楚这一点。 - Urs Beelibefore()
和after()
从未被触发,因此附加器从未被创建/附加,导致UnexpectedTestError出现。有什么想法我做错了什么吗?规则需要放置在特定的包中吗?另外,请将导入部分添加到您的答案中,因为某些对象/接口具有模糊的名称。 - Philzen虽然创建自定义的logback appender是一个不错的解决方案,但这只是第一步,最终你会开发/重复发明slf4j-test,如果你再深入一点: spf4j-slf4j-test,或其他我尚未知晓的框架。
最终你需要担心内存中保存的事件数量,当记录错误日志(而不是断言)时失败单元测试,使调试日志在测试失败时可用等等……
免责声明:我是spf4j-slf4j-test的作者,我编写了此后端以便更好地测试spf4j,这是一个寻找如何使用spf4j-slf4j-test的示例的好地方。我实现的主要优点之一是减少我的构建输出(这在Travis中有限制),同时仍然拥有我需要的所有详细信息,当发生故障时。
这是一个使用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);
}
}
在测试如“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匹配器库。
@Slf4j
注解,我按照以下方式操作。
@Slf4j
@Service
public class TesteeService {
Mono<ResponseEntity<HttpStatus>> sendRequest(RequestDto requestDto) {...logs an ERROR sometimes..}
public class MemoryLogAppender extends AppenderBase {
private final List<LogEvent> list = new ArrayList<>();
private final PatternLayoutEncoder encoder;
public MemoryLogAppender(LoggerContext loggerContext, String pattern) {
super.setContext(loggerContext);
this.encoder = new PatternLayoutEncoder();
this.encoder.setContext(loggerContext);
encoder.setPattern(pattern);
encoder.start();
}
@Override
protected void append(ILoggingEvent event) {
String msg = new String(encoder.encode(event));
LogEvent logEvent = new LogEvent(msg, event.getLevel(), event.getLoggerName());
list.add(logEvent);
}
/**
* Search for log message matching given regular expression.
*
* @param regex Regex to match
* @return matching log events
*/
public List<MemoryLogAppender.LogEvent> matches(String regex) {
return this.list.stream()
.filter(event -> event.message.matches(regex))
.collect(Collectors.toList());
}
class TestClass
private static MemoryLogAppender logAppender;
@BeforeAll
static void beforeAll() {
logAppender = new MemoryLogAppender((LoggerContext) LoggerFactory.getILoggerFactory(), "%-5level: %message");
logAppender.start();
}
@AfterAll
static void afterAll() {
logAppender.stop();
}
...
...
@Test
void bad_reference() throws Exception {
ResponseEntity<HttpStatus> response = testeeService.sendRequest(requestDto).block();
assertThat(logAppender.search("This reference is bad."))
.hasSize(1);
assertTrue(response.getStatusCode().is4xxClientError());
}
org.slf4j.LoggerFactory
的LoggerFactory
和ch.qos.logback.classic.Logger
的Logger
,当执行Logger fooLogger = (Logger) LoggerFactory.getLogger(Foo.class);
时会出现ClassCastException
异常。请帮我翻译这句话。 - hirenorg.junit.platform.commons.logging.LoggerFactory
,这就是我遇到的问题。 - Denis WashingtonSLF4J
,那么这个解决方案最终会引发SLF4J: Class path contains multiple SLF4J bindings.
警告,因为您同时拥有 SLF4J 和 logback.classic。 - Ghilteras