我有一些正在测试的代码,它调用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,但假设这是可能的。)
对于我来说,您可以通过使用JUnit和Mockito来简化您的测试。
我提出以下解决方案:
import org.apache.log4j.Appender;
import org.apache.log4j.Level;
import org.apache.log4j.LogManager;
import org.apache.log4j.spi.LoggingEvent;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.tuple;
import static org.mockito.Mockito.times;
@RunWith(MockitoJUnitRunner.class)
public class MyLogTest {
private static final String FIRST_MESSAGE = "First message";
private static final String SECOND_MESSAGE = "Second message";
@Mock private Appender appender;
@Captor private ArgumentCaptor<LoggingEvent> captor;
@InjectMocks private MyLog;
@Before
public void setUp() {
LogManager.getRootLogger().addAppender(appender);
}
@After
public void tearDown() {
LogManager.getRootLogger().removeAppender(appender);
}
@Test
public void shouldLogExactlyTwoMessages() {
testedClass.foo();
then(appender).should(times(2)).doAppend(captor.capture());
List<LoggingEvent> loggingEvents = captor.getAllValues();
assertThat(loggingEvents).extracting("level", "renderedMessage").containsExactly(
tuple(Level.INFO, FIRST_MESSAGE)
tuple(Level.INFO, SECOND_MESSAGE)
);
}
}
这就是为什么我们对不同消息量的测试有很好的灵活性
对于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所做的。
我创建了一个TestAppender类:
public class TestAppender extends AppenderBase<ILoggingEvent> {
private Stack<ILoggingEvent> events = new Stack<ILoggingEvent>();
@Override
protected void append(ILoggingEvent event) {
events.add(event);
}
public void clear() {
events.clear();
}
public ILoggingEvent getLastEvent() {
return events.pop();
}
}
然后在我的testng单元测试类的父类中创建了一个方法:
protected TestAppender testAppender;
@BeforeClass
public void setupLogsForTesting() {
Logger root = (Logger)LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
testAppender = (TestAppender)root.getAppender("TEST");
if (testAppender != null) {
testAppender.clear();
}
}
我在src/test/resources中定义了一个logback-test.xml文件,并添加了一个测试appender:
<appender name="TEST" class="com.intuit.icn.TestAppender">
<encoder>
<pattern>%m%n</pattern>
</encoder>
</appender>
并将这个appender添加到根appender:
<root>
<level value="error" />
<appender-ref ref="STDOUT" />
<appender-ref ref="TEST" />
</root>
现在,在从父测试类扩展而来的测试类中,我可以获得appender并记录最后一条消息,并验证消息、级别和throwable。
ILoggingEvent lastEvent = testAppender.getLastEvent();
assertEquals(lastEvent.getMessage(), "...");
assertEquals(lastEvent.getLevel(), Level.WARN);
assertEquals(lastEvent.getThrowableProxy().getMessage(), "...");
通过添加Appender进行单元测试并不能真正测试Logger的配置。因此,我认为这是一种独特的情况,在这种情况下,单元测试没有带来那么多价值,而集成测试带来了很多价值(特别是如果您的日志有一些审计目的)。
为了为它创建集成测试,让我们假设您正在运行一个简单的ConsoleAppender,并希望测试它的输出。然后,您应该测试如何将消息从System.out写入到它自己的ByteArrayOutputStream。
从这个意义上说,我会做以下事情(我使用JUnit 5):
public class Slf4jAuditLoggerTest {
private final ByteArrayOutputStream outContent = new ByteArrayOutputStream();
@BeforeEach
public void beforeEach() {
System.setOut(new PrintStream(outContent));
}
通过这种方式,你可以简单地测试它的输出:
@Test
public void myTest() {
// Given...
// When...
// Then
assertTrue(outContent.toString().contains("[INFO] My formatted string from Logger"));
}
如果你这样做了,你将为你的项目带来更多的价值,而不需要使用内存中的实现,创建一个新的Appender,或者其他什么。