如何做一个logging器中的消息断言JUnit

我有一些testing代码调用Javalogging器来报告其状态。 在JUnittesting代码中,我想validation是否在此logging器中创build了正确的日志条目。 大致如下:

methodUnderTest(bool x){ if(x) logger.info("x happened") } @Test tester(){ // perhaps setup a logger first. methodUnderTest(true); assertXXXXXX(loggedLevel(),Level.INFO); } 

我想这可以用一个特别改编的logging器(或处理程序,或格式化程序)来完成,但我宁愿重新使用已经存在的解决scheme。 (说实话,我不清楚如何从logging器获取logRecord,但假设这是可能的。)

我也需要这几次。 我已经在下面放了一个小样本,你要根据你的需要来调整。 基本上,你创build自己的Appender并将其添加到你想要的logging器。 如果你想收集所有的东西,根logging器是一个很好的开始,但是如果你愿意,你可以使用一个更具体的。 完成后不要忘记删除Appender,否则可能会造成内存泄漏。 下面我已经做了testing,但setUp@BeforetearDown@After可能是更好的地方,根据您的需要。

此外,下面的实现收集内存中的List中的所有内容。 如果你logging了很多,你可能会考虑添加一个filter来删除无聊的条目,或者将日志写入磁盘上的一个临时文件(提示: LoggingEventSerializable ,所以你应该能够序列化事件对象,如果你的日志消息是。)

 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); } } 

非常感谢这些(令人惊讶的)快速和有益的答案。 他们为我的解决scheme提供了正确的方法。

代码是我想要使用这个,使用java.util.logging作为其logging机制,并且我不觉得在这些代码中足够的家,以完全改变为log4j或logging器接口/外墙。 但是根据这些build议,我“篡改了”一个扩展程序,这就是一种享受。

一个简短的总结如下。 扩展java.util.logging.Handler

 class LogHandler extends Handler { Level lastLevel = Level.FINEST; public Level checkLevel() { return lastLevel; } public void publish(LogRecord record) { lastLevel = record.getLevel(); } public void close(){} public void flush(){} } 

很显然,你可以像LogRecord一样存储你喜欢/想要的东西,或者把它们全部推入堆栈,直到你发生溢出。

在junit-test的准备工作中,你需要创build一个java.util.logging.Logger并添加一个新的LogHandler给它:

 @Test tester() { Logger logger = Logger.getLogger("my junit-test logger"); LogHandler handler = new LogHandler(); handler.setLevel(Level.ALL); logger.setUseParentHandlers(false); logger.addHandler(handler); logger.setLevel(Level.ALL); 

setUseParentHandlers()的调用是为了setUseParentHandlers()正常的处理程序,所以(对于这个junittesting运行)不会发生不必要的日志logging。 做任何你的代码testing需要使用这个logging器,运行testing和assertEquality:

  libraryUnderTest.setLogger(logger); methodUnderTest(true); // see original question. assertEquals("Log level as expected?", Level.INFO, handler.checkLevel() ); } 

(当然,你会把这个工作的很大一部分转移到一个@Before方法中,并做了其他的改进,但是这会使这个表示变得混乱。

有效地,你正在testing一个依赖类的副作用。 对于unit testing,您只需要validation是否使用正确的参数调用了logger.info() 。 因此,我们模拟一个模拟logging器的框架,这将允许你testing你自己的类的行为。

嘲笑是一个select,虽然这很难,因为logging器通常是私有的静态最终 – 所以设置模拟logging器不会是小菜一碟,或者需要修改被testing的类。

你可以创build一个自定义的Appender(或者其他所谓的),并通过一个仅testing的configuration文件或者运行时(依赖于日志框架)来注册它。 然后你可以得到appender(静态的,如果在configuration文件中声明,或者通过它的当前引用,如果你插入运行时),并validation它的内容。

另一个select是嘲笑Appender,并validation消息是否logging到这个appender。 Log4j 1.2.x和mockito的示例:

 import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import org.apache.log4j.Appender; import org.apache.log4j.Level; import org.apache.log4j.Logger; import org.apache.log4j.spi.LoggingEvent; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; public class MyTest { private final Appender appender = mock(Appender.class); private final Logger logger = Logger.getRootLogger(); @Before public void setup() { logger.addAppender(appender); } @Test public void test() { // when Logger.getLogger(MyTest.class).info("Test"); // then ArgumentCaptor<LoggingEvent> argument = ArgumentCaptor.forClass(LoggingEvent.class); verify(appender).doAppend(argument.capture()); assertEquals(Level.INFO, argument.getValue().getLevel()); assertEquals("Test", argument.getValue().getMessage()); assertEquals("MyTest", argument.getValue().getLoggerName()); } @After public void cleanup() { logger.removeAppender(appender); } } 

这是我做的logback。

我创build了一个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(); } } 

然后在我的testngunit testingclass的家长中,我创build了一个方法:

 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文件,我添加了一个testingappender:

 <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> 

现在在我的父级testing类中扩展的testing类中,我可以获得appender并获取最后一条消息,并validation消息,级别和throwable。

 ILoggingEvent lastEvent = testAppender.getLastEvent(); assertEquals(lastEvent.getMessage(), "..."); assertEquals(lastEvent.getLevel(), Level.WARN); assertEquals(lastEvent.getThrowableProxy().getMessage(), "..."); 

受@ RonaldBlaschke的解决scheme启发,我想出了这个:

 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,但是我已经把它留在这里了。

正如其他人所提到的,你可以使用一个模拟框架。 为了做到这一点,你必须在你的课堂上暴露logging器(尽pipe我会优先考虑把它打包成私人的,而不是创build一个公开的二传手)。

另一种解决scheme是手工制作假logging器。 你必须写假logging器(更多的夹具代码),但在这种情况下,我更喜欢testing的增强的可读性从模拟框架保存的代码。

我会做这样的事情:

 class FakeLogger implements ILogger { public List<String> infos = new ArrayList<String>(); public List<String> errors = new ArrayList<String>(); public void info(String message) { infos.add(message); } public void error(String message) { errors.add(message); } } class TestMyClass { private MyClass myClass; private FakeLogger logger; @Before public void setUp() throws Exception { myClass = new MyClass(); logger = new FakeLogger(); myClass.logger = logger; } @Test public void testMyMethod() { myClass.myMethod(true); assertEquals(1, logger.infos.size()); } } 

值得一提的另一个想法,虽然这是一个较老的话题,但是创build一个CDI制作者来注入你的logging器,这样嘲笑就变得容易了。 (而且它还具有不需要再声明“整个logging器声明”的优点,但这是无关紧要的)

例:

创buildlogging器注入:

 public class CdiResources { @Produces @LoggerType public Logger createLogger(final InjectionPoint ip) { return Logger.getLogger(ip.getMember().getDeclaringClass()); } } 

限定词:

 @Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({TYPE, METHOD, FIELD, PARAMETER}) public @interface LoggerType { } 

在生产代码中使用logging器:

 public class ProductionCode { @Inject @LoggerType private Logger logger; public void logSomething() { logger.info("something"); } } 

在testing代​​码中testinglogging器(给出一个easyMock示例):

 @TestSubject private ProductionCode productionCode = new ProductionCode(); @Mock private Logger logger; @Test public void testTheLogger() { logger.info("something"); replayAll(); productionCode.logSomething(); } 

使用Jmockit(1.21)我能够写这个简单的testing。 testing确保一个特定的ERROR消息被调用一次。

 @Test public void testErrorMessage() { final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger( MyConfig.class ); new Expectations(logger) {{ //make sure this error is happens just once. logger.error( "Something went wrong..." ); times = 1; }}; new MyTestObject().runSomethingWrong( "aaa" ); //SUT that eventually cause the error in the log. } 

嘲笑Appender可以帮助捕获日志行。 查找示例: http : //clearqa.blogspot.co.uk/2016/12/test-log-lines.html

 // Fully working test at: https://github.com/njaiswal/logLineTester/blob/master/src/test/java/com/nj/Utils/UtilsTest.java @Test public void testUtilsLog() throws InterruptedException { Logger utilsLogger = (Logger) LoggerFactory.getLogger("com.nj.utils"); final Appender mockAppender = mock(Appender.class); when(mockAppender.getName()).thenReturn("MOCK"); utilsLogger.addAppender(mockAppender); final List<String> capturedLogs = Collections.synchronizedList(new ArrayList<>()); final CountDownLatch latch = new CountDownLatch(3); //Capture logs doAnswer((invocation) -> { LoggingEvent loggingEvent = invocation.getArgumentAt(0, LoggingEvent.class); capturedLogs.add(loggingEvent.getFormattedMessage()); latch.countDown(); return null; }).when(mockAppender).doAppend(any()); //Call method which will do logging to be tested Application.main(null); //Wait 5 seconds for latch to be true. That means 3 log lines were logged assertThat(latch.await(5L, TimeUnit.SECONDS), is(true)); //Now assert the captured logs assertThat(capturedLogs, hasItem(containsString("One"))); assertThat(capturedLogs, hasItem(containsString("Two"))); assertThat(capturedLogs, hasItem(containsString("Three"))); } 

至于我,你可以通过在Mockito使用JUnit来简化你的testing。 我提出以下解决scheme:

 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) ); } } 

这就是为什么我们有不同的消息数量testing的灵活性

使用下面的代码。 我正在使用相同的代码为我的spring集成testing,我正在使用日志logging。 使用方法assertJobIsScheduled来断言在日志中打印的文本。

 import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.spi.LoggingEvent; import ch.qos.logback.core.Appender; private Logger rootLogger; final Appender mockAppender = mock(Appender.class); @Before public void setUp() throws Exception { initMocks(this); when(mockAppender.getName()).thenReturn("MOCK"); rootLogger = (Logger) LoggerFactory.getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME); rootLogger.addAppender(mockAppender); } private void assertJobIsScheduled(final String matcherText) { verify(mockAppender).doAppend(argThat(new ArgumentMatcher() { @Override public boolean matches(final Object argument) { return ((LoggingEvent)argument).getFormattedMessage().contains(matcherText); } })); } 

对于log4j2,解决scheme略有不同,因为AppenderSkeleton不再可用。 此外,如果您期待多个日志logging消息,则使用Mockito或类似的库创build带有ArgumentCaptor的Appender将不起作用,因为MutableLogEvent会在多个日志消息中重复使用。 我发现log4j2的最佳解决scheme是:

 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()); } } 

如果你正在使用java.util.logging.Logger这篇文章可能会非常有帮助,它会创build一个新的处理程序并在日志输出中进行断言: http : //octodecillion.com/blog/jmockit-test-logging/