我有一些正在测试的代码,它调用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,但假设这是可能的。)
对于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,来自https://www.dontpanicblog.co.uk/2018/04/29/test-log4j2-with-junit/的解决方案允许我断言消息已被记录。
解决方案是这样的:
Define a log4j appender as an ExternalResource rule
public class LogAppenderResource extends ExternalResource {
private static final String APPENDER_NAME = "log4jRuleAppender";
/**
* Logged messages contains level and message only.
* This allows us to test that level and message are set.
*/
private static final String PATTERN = "%-5level %msg";
private Logger logger;
private Appender appender;
private final CharArrayWriter outContent = new CharArrayWriter();
public LogAppenderResource(org.apache.logging.log4j.Logger logger) {
this.logger = (org.apache.logging.log4j.core.Logger)logger;
}
@Override
protected void before() {
StringLayout layout = PatternLayout.newBuilder().withPattern(PATTERN).build();
appender = WriterAppender.newBuilder()
.setTarget(outContent)
.setLayout(layout)
.setName(APPENDER_NAME).build();
appender.start();
logger.addAppender(appender);
}
@Override
protected void after() {
logger.removeAppender(appender);
}
public String getOutput() {
return outContent.toString();
}
}
Define a test that use your ExternalResource rule
public class LoggingTextListenerTest {
@Rule public LogAppenderResource appender = new LogAppenderResource(LogManager.getLogger(LoggingTextListener.class));
private LoggingTextListener listener = new LoggingTextListener(); // Class under test
@Test
public void startedEvent_isLogged() {
listener.started();
assertThat(appender.getOutput(), containsString("started"));
}
}
不要忘记将log4j2.xml作为src/test/resources的一部分
受到@RonaldBlaschke的解决方案的启发,我想到了这个:
public class Log4JTester extends ExternalResource {
TestAppender appender;
@Override
protected void before() {
appender = new TestAppender();
final Logger rootLogger = Logger.getRootLogger();
rootLogger.addAppender(appender);
}
@Override
protected void after() {
final Logger rootLogger = Logger.getRootLogger();
rootLogger.removeAppender(appender);
}
public void assertLogged(Matcher<String> matcher) {
for(LoggingEvent event : appender.events) {
if(matcher.matches(event.getMessage())) {
return;
}
}
fail("No event matches " + matcher);
}
private static class TestAppender extends AppenderSkeleton {
List<LoggingEvent> events = new ArrayList<LoggingEvent>();
@Override
protected void append(LoggingEvent event) {
events.add(event);
}
@Override
public void close() {
}
@Override
public boolean requiresLayout() {
return false;
}
}
}
... 这允许你做:
@Rule public Log4JTester logTest = new Log4JTester();
@Test
public void testFoo() {
user.setStatus(Status.PREMIUM);
logTest.assertLogged(
stringContains("Note added to account: premium customer"));
}
你也许可以用更聪明的方式来使用hamcrest,但我就讲到这里。