如何做一个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
或@Before
和tearDown
或@After
可能是更好的地方,根据您的需要。
此外,下面的实现收集内存中的List
中的所有内容。 如果你logging了很多,你可能会考虑添加一个filter来删除无聊的条目,或者将日志写入磁盘上的一个临时文件(提示: LoggingEvent
是Serializable
,所以你应该能够序列化事件对象,如果你的日志消息是。)
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/