In previous articles, I’ve described how to test System.out with JUnit and how to test log4j with JUnit. This article describes how to test log4j2 logging with JUnit. This largely follows the same technique as testing log4j logging but using the the newer log4j2 APIs.
Differences between log4j and log4j2
log4j2 follows a similar design to log4j but has the following differences:
- Package structure: The root package name has changed fromĀ org.apache.log4j to org.apache.logging.log4j. The core implementation classes (Logger, Appender ,Layout etc.) are in org.apache.log4j.logging.core.
- Layout and Appender instances are created with Builder classes instead of constructors.
- The WriterAppender class that we use to capture log output appends to a Writer instead of an OutputStream.
- Configuration has changed. The old log4j.properties / log4j.xml config file is replaced with log4j2 equivalents and JSON and YAML formats are now supported.
JUnit @Rule
The JUnit ExternalResource described in the previous article now looks like this:
package org.vxmlriot.jvoicexml.junit; import org.apache.logging.log4j.core.Appender; import org.apache.logging.log4j.core.Logger; import org.apache.logging.log4j.core.StringLayout; import org.apache.logging.log4j.core.appender.WriterAppender; import org.apache.logging.log4j.core.layout.PatternLayout; import org.junit.rules.ExternalResource; import java.io.CharArrayWriter; /** * JUnit rule for testing output to Log4j. Handy for verifying logging. * This sets up and tears down an Appender resource on a given Logger. */ public class LogAppenderResource extends ExternalResource { private static final String APPENDER_NAME = "log4jRuleAppender"; /** * Logged messages contains level and message only. * This allows us to test that level and message are set. */ private static final String PATTERN = "%-5level %msg"; private Logger logger; private Appender appender; private final CharArrayWriter outContent = new CharArrayWriter(); public LogAppenderResource(org.apache.logging.log4j.Logger logger) { this.logger = (org.apache.logging.log4j.core.Logger)logger; } @Override protected void before() { StringLayout layout = PatternLayout.newBuilder().withPattern(PATTERN).build(); appender = WriterAppender.newBuilder() .setTarget(outContent) .setLayout(layout) .setName(APPENDER_NAME).build(); appender.start(); logger.addAppender(appender); } @Override protected void after() { logger.removeAppender(appender); } public String getOutput() { return outContent.toString(); } }
Note in particular the difference in the way that the Layout and Appender are constructed.
This JUnit ExternalResource rule can be used in a test in exactly the same way as before:
public class LoggingTextListenerTest { @Rule public LogAppenderResource appender = new LogAppenderResource(LogManager.getLogger(LoggingTextListener.class)); private LoggingTextListener listener = new LoggingTextListener(); // Class under test @Test public void startedEvent_isLogged() { listener.started(); assertThat(appender.getOutput(), containsString("started")); } }
Example
I used this technique when upgrading the VoiceXMLRiot project from log4j to log4j2. Check the code in GitHub for examples:
- LogAppenderResource: The JUnit @Rule to support logger tests
- LoggingTextListenerTest: The JUnit test class using the LogAppenderResource @Rule
- LoggingTextListener: The class under test. This writes to the log when events are received.
Thank you! I tried these classes but outContent (return outContent.toString();) is always empty. Am i missing anything?
never mind, wrong log level.
the same as mentioned, in all log levels result is empty string “”.
Thank you so much Stuart! It worked for my project. However to make it works like a charm I have to make some small changes to the customised Appender class. Here are my changes:
– Set the PATTERN to this: “%-5level %m%n”
– Added one extra method to the class Appender to get appropriate log messages, eg: error, debug, infor or warning. Here is the implementation:
public List getLogMessageListByLevel(Level level) {
String[] logMessages = charArrayWriter.toString().split("\\n");
if (level == Level.ALL) {
return Arrays.asList(logMessages);
}
List returnedList = new ArrayList();
for (String msg: logMessages) {
if (msg.contains(level.name())) {
returnedList.add(msg);
}
}
return returnedList;
}
Could you please share the whole class
Working example on GitHub. Here’s the full test class:
https://github.com/hotblac/voicexmlriot/blob/a59e2a50296ec8c3139cb38d2d9e737a7e5c5b49/voicexmlriot-driver/src/test/java/org/vxmlriot/jvoicexml/listener/LoggingTextListenerTest.java
[…] https://www.dontpanicblog.co.uk/2018/04/29/test-log4j2-with-junit/ […]