On my current project, we’re using the logback framework (behind SL4J) to do logging. For some parts of our system, it was particularly important some information made their way into the log files, and so we wanted to not test the correct output. Rather than do it with interaction based tests, I followed the pattern that I described in a previous post.
Here’s a test I might write (note that I’m writing the test in a way to actually test the appender behaviour in this case because my domain class doesn’t nothing special):
package com.thekua.spikes; import org.junit.After; import org.junit.Test; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; public class LogbackCapturingAppenderTest { @After public void cleanUp() { LogbackCapturingAppender.Factory.cleanUp(); } @Test public void shouldCaptureAGivenLog() throws Exception { // Given LogbackCapturingAppender capturing = LogbackCapturingAppender.Factory.weaveInto(OurDomainWithLogger.LOG); OurDomainWithLogger domainClass = new OurDomainWithLogger(); // when domainClass.logThis("This should be logged"); // then assertThat(capturing.getCapturedLogMessage(), is("This should be logged")); } @Test public void shouldNotCaptureAGiveLogAfterCleanUp() throws Exception { // Given LogbackCapturingAppender capturing = LogbackCapturingAppender.Factory.weaveInto(OurDomainWithLogger.LOG); OurDomainWithLogger domainClass = new OurDomainWithLogger(); domainClass.logThis("This should be logged at info"); LogbackCapturingAppender.Factory.cleanUp(); // when domainClass.logThis("This should not be logged"); // then assertThat(capturing.getCapturedLogMessage(), is("This should be logged at info")); } }
And the corresponding Logback appender used in tests.
package com.thekua.spikes; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.Appender; import ch.qos.logback.core.AppenderBase; import java.util.ArrayList; import java.util.List; public class LogbackCapturingAppender extends AppenderBase<ILoggingEvent> { public static class Factory { private static List<LogbackCapturingAppender> ALL = new ArrayList<LogbackCapturingAppender>(); public static LogbackCapturingAppender weaveInto(org.slf4j.Logger sl4jLogger) { LogbackCapturingAppender appender = new LogbackCapturingAppender(sl4jLogger); ALL.add(appender); return appender; } public static void cleanUp() { for (LogbackCapturingAppender appender : ALL) { appender.cleanUp(); } } } private final Logger logger; private ILoggingEvent captured; public LogbackCapturingAppender(org.slf4j.Logger sl4jLogger) { this.logger = (Logger) sl4jLogger; connect(logger); detachDefaultConsoleAppender(); } private void detachDefaultConsoleAppender() { Logger rootLogger = getRootLogger(); Appender<ILoggingEvent> consoleAppender = rootLogger.getAppender("console"); rootLogger.detachAppender(consoleAppender); } private Logger getRootLogger() { return logger.getLoggerContext().getLogger("ROOT"); } private void connect(Logger logger) { logger.setLevel(Level.ALL); logger.addAppender(this); this.start(); } public String getCapturedLogMessage() { return captured.getMessage(); } @Override protected void append(ILoggingEvent iLoggingEvent) { captured = iLoggingEvent; } private void cleanUp() { logger.detachAppender(this); } }
I thought it’d be useful to share this and I’ve created a github project to host the code.