我有一些正在测试的代码,它调用Java记录器来报告其状态。
在JUnit测试代码中,我想验证在这个日志记录器中创建了正确的日志条目。大致如下:
methodUnderTest(bool x){
if(x)
logger.info("x happened")
}
@Test tester(){
// perhaps setup a logger first.
methodUnderTest(true);
assertXXXXXX(loggedLevel(),Level.INFO);
}
我认为这可以用一个经过特别调整的记录器(或处理程序或格式化程序)来完成,但我更愿意重用现有的解决方案。(而且,老实说,我不清楚如何从记录器获得logRecord,但假设这是可能的。)
I've needed this several times as well. I've put together a small sample below, which you'd want to adjust to your needs. Basically, you create your own Appender and add it to the logger you want. If you'd want to collect everything, the root logger is a good place to start, but you can use a more specific if you'd like. Don't forget to remove the Appender when you're done, otherwise you might create a memory leak. Below I've done it within the test, but setUp or @Before and tearDown or @After might be better places, depending on your needs.
此外,下面的实现将所有内容收集到内存中的List中。如果您记录了很多日志,您可能会考虑添加一个过滤器来删除无聊的条目,或者将日志写入磁盘上的临时文件(提示:LoggingEvent是可序列化的,因此如果您的日志消息是可序列化的,那么您应该能够序列化事件对象)。
import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.spi.LoggingEvent;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
public class MyTest {
@Test
public void test() {
final TestAppender appender = new TestAppender();
final Logger logger = Logger.getRootLogger();
logger.addAppender(appender);
try {
Logger.getLogger(MyTest.class).info("Test");
}
finally {
logger.removeAppender(appender);
}
final List<LoggingEvent> log = appender.getLog();
final LoggingEvent firstLogEntry = log.get(0);
assertThat(firstLogEntry.getLevel(), is(Level.INFO));
assertThat((String) firstLogEntry.getMessage(), is("Test"));
assertThat(firstLogEntry.getLoggerName(), is("MyTest"));
}
}
class TestAppender extends AppenderSkeleton {
private final List<LoggingEvent> log = new ArrayList<LoggingEvent>();
@Override
public boolean requiresLayout() {
return false;
}
@Override
protected void append(final LoggingEvent loggingEvent) {
log.add(loggingEvent);
}
@Override
public void close() {
}
public List<LoggingEvent> getLog() {
return new ArrayList<LoggingEvent>(log);
}
}
对于log4j2,解决方案略有不同,因为AppenderSkeleton不再可用。此外,使用Mockito或类似的库来创建带有ArgumentCaptor的Appender将无法工作,因为MutableLogEvent在多个日志消息上被重用。我为log4j2找到的最佳解决方案是:
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.Logger;
import org.apache.logging.log4j.core.appender.AbstractAppender;
private static MockedAppender mockedAppender;
private static Logger logger;
@Before
public void setup() {
mockedAppender.message.clear();
}
/**
* For some reason mvn test will not work if this is @Before, but in eclipse it works! As a
* result, we use @BeforeClass.
*/
@BeforeClass
public static void setupClass() {
mockedAppender = new MockedAppender();
logger = (Logger)LogManager.getLogger(MatchingMetricsLogger.class);
logger.addAppender(mockedAppender);
logger.setLevel(Level.INFO);
}
@AfterClass
public static void teardown() {
logger.removeAppender(mockedAppender);
}
@Test
public void test() {
// do something that causes logs
for (String e : mockedAppender.message) {
// add asserts for the log messages
}
}
private static class MockedAppender extends AbstractAppender {
List<String> message = new ArrayList<>();
protected MockedAppender() {
super("MockedAppender", null, null);
}
@Override
public void append(LogEvent event) {
message.add(event.getMessage().getFormattedMessage());
}
}
这里有一个简单有效的Logback解决方案。
它不需要添加/创建任何新类。
它依赖于ListAppender:一个白盒回logback appender,其中将日志条目添加到公共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
// addAppender is outdated now
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());
}
}
JUnit断言听起来不太适合断言列表元素的某些特定属性。
像AssertJ或Hamcrest这样的Matcher/断言库似乎更好:
使用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));
Log4J2的API略有不同。你也可以使用它的async appender。我为此创建了一个锁定的appender:
public static class LatchedAppender extends AbstractAppender implements AutoCloseable {
private final List<LogEvent> messages = new ArrayList<>();
private final CountDownLatch latch;
private final LoggerConfig loggerConfig;
public LatchedAppender(Class<?> classThatLogs, int expectedMessages) {
this(classThatLogs, null, null, expectedMessages);
}
public LatchedAppender(Class<?> classThatLogs, Filter filter, Layout<? extends Serializable> layout, int expectedMessages) {
super(classThatLogs.getName()+"."+"LatchedAppender", filter, layout);
latch = new CountDownLatch(expectedMessages);
final LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
final Configuration config = ctx.getConfiguration();
loggerConfig = config.getLoggerConfig(LogManager.getLogger(classThatLogs).getName());
loggerConfig.addAppender(this, Level.ALL, ThresholdFilter.createFilter(Level.ALL, null, null));
start();
}
@Override
public void append(LogEvent event) {
messages.add(event);
latch.countDown();
}
public List<LogEvent> awaitMessages() throws InterruptedException {
assertTrue(latch.await(10, TimeUnit.SECONDS));
return messages;
}
@Override
public void close() {
stop();
loggerConfig.removeAppender(this.getName());
}
}
像这样使用它:
try (LatchedAppender appender = new LatchedAppender(ClassUnderTest.class, 1)) {
ClassUnderTest.methodThatLogs();
List<LogEvent> events = appender.awaitMessages();
assertEquals(1, events.size());
//more assertions here
}//appender removed