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.
Hey Patrick, this was very helpful! A lot of arcane connections needed to be made and I appreciate you doing this. Thanks!
Thanks for your help Patrick!
Thanks for sharing! I used to implement unit tests for logging using SLF4J Test, but excluding dependencies in maven is always a bit fiddly with IDEs like IntelliJ and eclipse (not with Netbeans, of course). Your approach stated here is super simple, that great.
Could you provide this as reusable maven dependency? I opened an issue at your Repo: https://github.com/thekua/Sample-Code/issues/3
Thanks!
java.lang.ClassCastException: org.slf4j.impl.Log4jLoggerAdapter cannot be cast to ch.qos.logback.classic.Logger