Skip to content

Instantly share code, notes, and snippets.

@davidkuster
Last active July 1, 2020 14:27
Show Gist options
  • Save davidkuster/8bef03ab0b873f7b06b6796e1589b51a to your computer and use it in GitHub Desktop.
Save davidkuster/8bef03ab0b873f7b06b6796e1589b51a to your computer and use it in GitHub Desktop.
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;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment