package tbd; 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; import org.slf4j.LoggerFactory; import java.util.Arrays; import java.util.List; import java.util.function.Consumer; /** * Utility to enable verification of log messages in tests. * * Usage is as follows: * * //@Test * void shouldWriteLogMessage() { * new TestLogger().with(testLogger -> { * new ClassUnderTest.someCodeThatIncludesLogStatements(); * * testLogger.contains(Level.INFO, "Log message says: {}", "param to log message"); * }); * } */ public class TestLogger { private final Logger logger; private final ListAppender<ILoggingEvent> listAppender; public TestLogger() { this.logger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); this.listAppender = new ListAppender<>(); this.listAppender.start(); this.logger.addAppender(this.listAppender); } /** * This method wraps the given function with a Logback ListAppender, and provides a * reference to that list appender to the calling function. Log messages can then be * validated using the provided list appender reference. * * Example: * * new TestLogger().with(testLogger -> { * new ClassUnderTest().someCodeThatIncludesLogStatements(); * * assertTrue(testLogger.contains(Level.DEBUG, "expected log message")); * }); * * NOTE: * * The following example will not work. The test logging needs to be in place * before the loggers in the class under test are initialized. * * ClassUnderTest cut = new ClassUnderTest(); * new TestLogger().with(testLogger -> { * cut.someCodeThatIncludesLogStatements(); * * assertTrue(testLogger.contains(Level.DEBUG, "expected log message")); * }); */ public void with(Consumer<TestLogger> function) { try { function.accept(this); } finally { logger.detachAppender(listAppender); } } /** * Determines whether a message was logged at the given level, with the given string, * and the given parameters. */ public boolean contains(Level level, String message, Object... params) { return listAppender .list .stream() .anyMatch(event -> { // uncomment for debugging //System.out.println( // "message = " + event.getMessage() // + ", args = " + Arrays.deepToString(event.getArgumentArray()) // + ", params = " + Arrays.deepToString(params)); return level.equals(event.getLevel()) && message.equals(event.getMessage()) // arg array defaults to null and params defaults to empty array && ((event.getArgumentArray() == null && params.length == 0) || Arrays.equals(params, event.getArgumentArray())); }); } // TODO: determine if it's worth validating the class of exception logged also. // Note that it's not possible to have another method param after the Object... varargs above, // so parameter order to a separate containsWithException() [or whatever] method would have // to be different than the existing contains() method. /** * Returns the list of captured ILoggingEvents, for debugging or deeper verification * than provided by the contains() method above. */ public List<ILoggingEvent> getLogEvents() { return listAppender.list; } }