Testing Log Messages Created by Slf4j Without Using Log4J - java

I want to test a method whether it's creating correct logs or not. My class looks like this:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;
class EventLogHandler {
private final Logger logger = LoggerFactory.getLogger(EventLogHandler.class);
private final Marker eventMarker = MarkerFactory.getMarker("EVENT");
public void handle(final Event event) {
final String log = SomeOtherClass.createLog(event);
logger.info(eventMarker, log);
}
}
I've seen some examples/solutions for testing logs but all of them are using Log4j, which we are not using in the project. I can only use Log4j2 of spring-boot, Slf4j and Logback classic.
How can I test that handle(...) method with my existing dependencies?

With the current implementation you cannot test/verify the invocations on logger without engaging a logging system and asserting on its output e.g. introduce logback and configure it with a stdout appender and capture stdout and assert against it etc.
In order to test your class without doing any of that you have to get your hands on the logger instance in use in EventLogHandler. The current implementation makes this difficult by constructing the logger like this:
private final Logger logger = LoggerFactory.getLogger(EventLogHandler.class);
A common approach to testing in this scenario is to refactor the creation of logger in such a way that you can inject a mocked instance into EventLogHandler when running your tests. For example:
class EventLogHandler {
private final Marker eventMarker = MarkerFactory.getMarker("EVENT");
private final Logger logger;
public EventLogHandler() {
this(LoggerFactory.getLogger(EventLogHandler.class));
}
// probably only used by your test case
public EventLogHandler(Logger logger) {
this.logger = logger;
}
public void handle(final Event event) {
logger.info(eventMarker, log);
}
}
Then test it like so:
#Test
public void someTest() {
Logger logger = Mockito.mock(Logger.class);
EventLogHandler sut = new EventLogHandler(logger);
sut.handle(event);
// verify that the right state is extracted from the given event and that the correct marker is used
Mockito.verify(logger).info(..., ...);
}
A less common alternative would be to use Powermock to allow you to mock this call: LoggerFactory.getLogger(EventLogHandler.class); and then use Mockito to verify calls onto it in the same way as is shown above..

Related

Need a better way to keep test code out of production code - slf4jtesting

I'm using com.portingle:slf4jtesting:1.1.3 to aid testing of some logging functionality.
My problem is that the devs at com.portingle are strong advocates of dependency injection and suggest only dependency injection for utilising their slf4jtesting::ILoggerFactory utility (an implementation of slf4j which stores up the log entries for easy testing and validation).
With dependency injection, I could just create my slf4j loggers in my classes like this and inject either a production or test LoggerFactory:
import org.slf4j.ILoggerFactory;
import org.slf4j.Logger;
public class Example1 {
private final Logger logger;
public Example1(ILoggerFactory lf) {
this.logger = lf.getLogger(Example1.class.getName());
}
public void aMethodThatLogs() {
logger.info("Hello World!");
}
}
Reasonable enough but I've got a legacy app and all my loggers are already coded and used sometimes in static code blocks / methods, so standard DI constructor injection won't work.
Currently I am doing this:
private static final Logger log = LoggingUtils.getLogger(
RequestLoggingFilter.class);
and LoggingUtils looks like this:
public class LoggingUtils {
private LoggingUtils() {
}
private static ILoggerFactory iLoggerFactory =
LoggerFactory.getILoggerFactory();
/**
* We don't want to call this in production.
*/
public static void switchToTestLogging() {
iLoggerFactory = Settings.instance().enableAll().buildLogging();
}
/**
* Return logger for a class, of whatever implementation is running,
* e.g. test or prod logger.
*
* #param loggingClass the class doing the logging
* #return logger
*/
public static Logger getLogger(Class loggingClass) {
return iLoggerFactory.getLogger(loggingClass.getName());
}
So in tests, I can switch to slf4jtesting::ILoggerFactory with a call to my switchToTestLogging(), but the end result is that I have slf4jtesting code in my production code.
Alternatively, I could make the iLoggerFactory public so that the tests can just replace it when necessary, but it would be bad practice to allow any production code to do that.
Lastly, I could use reflection to hack the private ILoggerFactory instance in my LoggingUtils class and assign a test LoggerFactory during testing:
#BeforeAll
public static void setupLogging()
throws NoSuchFieldException, IllegalAccessException {
Field loggerFactoryField =
LoggingUtils.class.getDeclaredField("iLoggerFactory");
loggerFactoryField.setAccessible(true);
loggerFactoryField.set(null,
Settings.instance().enableAll().buildLogging());
}
but that's also not exactly 'best practice'.
Is there any way to keep the ILoggerFactory instance private, avoid reflection and keep the test libraries out of production?
I am not a big fan of static coupling, but technically, you are focussing too much on implementation concerns.
You can remove switchToTestLogging altogether
public class LoggingUtils {
private LoggingUtils() {
}
private static ILoggerFactory iLoggerFactory;
/**
* Return logger for a class
*
* #param loggingClass the class doing the logging
* #return logger
*/
public static Logger getLogger(Class loggingClass) {
//Lazy loading.
if(iLoggerFactory == null) {
iLoggerFactory = LoggerFactory.getILoggerFactory();
}
return iLoggerFactory.getLogger(loggingClass.getName());
}
}
and in testing mock the factory method to return the desired logger when invoked.
PowerMockito should be able to let you mock static members.
#RunWith(PowerMockRunner.class)
#PrepareForTest(LoggingUtils.class) //<-- important
public class SomeTest {
#Test
public void someTestMethod() {
//Arrange
//get the logger used in testing
ILoggerFactory testLoggerFactory = Settings.instance().enableAll().buildLogging();
//set up util for mocking
PowerMockito.mockStatic(LoggingUtils.class);
//setup mocked member
Mockito.when(LoggingUtils.getLogger(any(Class.class)))
.thenAnswer(i -> testLoggerFactory.getLogger(i.getArguments()[0].getName()));
//Act
//call subject under test that is coupled to LoggingUtils
//Assert
//...
}
}
The LoggingUtils is now only concerned with production concerns, and PowerMockito allows you to stub test loggers whenever LoggingUtils.getLogger is invoked while exercising tests.
Disclaimer: This has not been tested. Provided based on my recollection of the framework.
With that done I would strongly advise refactoring your code to follow my SOLID practices that would make your code cleaner and more maintainable. Hacks like these are code smells and a clear indicator of poor design. Just because there are tools that allow a work around does not take away from the poor design choices made.

Problems using JMockit with log4j 2.9.0

I'm trying to use JMockit to test that a certain logging operation takes place:
public class LogClass1 {
public void doLog() {
Logger logger = LogManager.getLogger(LogClass1.class);
logger.info("This is a log message for {}", "arg1");
}
}
public class LogClass1Test {
#Mocked
private Logger logger;
#Tested
private LogClass1 x;
#Before
public void setup() {
x = new LogClass1();
}
#Test
public void testDoLog() {
new Expectations() {
{
logger.info("This is a log message for {}", "arg1");
}
};
x.doLog();
}
}
But this results in a "missing 1 invocation to org.apache.logging.log4j.Logger#info" error.
I've done similar mocking with log4j 1.x in the past, and I haven't had this problem. I'm wondering if there's some issue because log4j 2.x seems to have many more overloads of its info() methods.
I tried changing "arg1" to (Object)"arg1" in the unit test to see if I could get it to match the signature. This didn't help.
Any thoughts on how I can get this to work?
Note that Logger is an interface, and that LogClass1 obtains an instance of it through the LogManager.getLogger static factory method. So, obviously, it creates an instance of some Logger implementation class. And said class is not being mocked in the test.
What the test needs to do is to mock LogManager, so it returns the #Mocked Logger instance. That is, add a #Mocked LogManager field to the test class.
(Also, no need for that setup method since #Tested creates an instance automatically.)

May I create logger in Log4j2 per Level, not per class?

I was taking over an old java projcet. And the original designer has gone.
In this project, it uses Log4j in a static way, like this:
public class LogUtil {
private static final String FQCN = LogUtil.class.getName();
private static final Logger logger1 = Logger.getLogger("INFO");
private static final Logger logger2 = Logger.getLogger("ERROR");
public static void info(Object message) {
if (logger1.isInfoEnabled()) {
forcedLog(logger1, Level.INFO, message);
}
}
public static void error(Object message) {
forcedLog(logger2, Level.ERROR, message);
}
private static void forcedLog(Logger logger, Level level, Object message) {
logger.callAppenders(new LoggingEvent(FQCN, logger, level, message, null));
}
}
Then other class can easily write logs with LogUtil.error("") and LogUtil.info("") .
And the INFO logger and ERROR logger were configured to write in separate files.
But in Log4j2, the class LoggingEvent can not be found.
So the question is, if I want to use Log4j2, how should I modify this class?
(maybe use Log4j 1.x bridge provided in log4j2 packages?)
Is using logger in this way reasonable? It seems that it is easier than creating loggers per class. Is there any problems like performance or others?
So the question is, if I want to use Log4j2, how should I modify this class? (maybe use Log4j 1.x bridge provided in log4j2 packages?)
You could rewrite it to use the public API instead of relying on underlying implementation details. So, something like this:
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class LogUtil {
private static final Logger TRACE_LOGGER = LogManager.getLogger("TRACE");
private static final Logger DEBUG_LOGGER = LogManager.getLogger("DEBUG");
private static final Logger INFO_LOGGER = LogManager.getLogger("INFO");
private static final Logger WARN_LOGGER = LogManager.getLogger("WARN");
private static final Logger ERROR_LOGGER = LogManager.getLogger("ERROR");
private static final Logger FATAL_LOGGER = LogManager.getLogger("FATAL");
public static void trace(Object msg){
TRACE_LOGGER.trace(msg);
}
public static void debug(Object msg){
DEBUG_LOGGER.debug(msg);
}
public static void info(Object msg) {
INFO_LOGGER.info(msg);
}
public static void warn(Object msg){
WARN_LOGGER.warn(msg);
}
public static void error(Object msg) {
ERROR_LOGGER.error(msg);
}
public static void fatal(Object msg){
FATAL_LOGGER.fatal(msg);
}
...
}
Is using logger in this way reasonable? It seems that it is easier than creating loggers per class. Is there any problems like performance or others?
I don't see how adding 3 lines to a class is anything other than easy. All it takes to create a logger (in log4j2) is this:
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
...
private static final Logger logger = LogManager.getLogger();
As for potential problems, with this implementation you give up a lot of the functionality of log4j.
You can't change the log level on a package or class basis because all of the classes share the same loggers. Perhaps you want to turn on TRACE logging for a new class you're working on, but you don't want any TRACE logs from other classes as it clutters the log file - you would not be able to make this kind of change easily.
You can't change the appenders on a package or class basis because once again all classes share the same loggers. Perhaps you want to send all of the logs from a new package that you're working on to a separate file so that you can more easily see what your new code is doing without having to filter the logs manually. Maybe you want to see all logs from a particular class or package in a single file during development so that you can follow the execution more easily without having to jump around to different files.

Proper method for registering custom LoggerFactory for Log4j 1.2?

I'm working on a web application that uses Log4j (1.2.16) logging through out. To avoid having to edit a ton of files, I am trying to hook in a custom LoggerFactory implementation that protects against log forging.
It appears one can set the log4j.loggerFactory configuration setting (project uses a property file method) to specify the logger factory to use. However, this does not appear to work. After examining the source code for Log4j, it appears that the property is never really used, even though it is read by the PropertyConfigurator class.
Examining more of the Log4j source, it appears the only way to achieve what I want, I have to create custom sub-classes of Log4j classes. Is this the only way?
The following represents what I had to do in my web application context listener to initialize Log4j so my custom logger factory will be used:
package com.example.myapp.log4j;
import javax.servlet.ServletContextListener;
import javax.servlet.ServletContextEvent;
import javax.servlet.*;
public class MyLog4jInitContextListener implements ServletContextListener
{
public void contextInitialized(
ServletContextEvent event
) {
this.context = event.getServletContext();
String file = context.getInitParameter("log4jConfiguration");
if (file != null) {
String prefix = context.getRealPath("/");
String pathname = prefix+file;
event.getServletContext().log("Initializing log4j with "+pathname);
org.apache.log4j.LogManager.setRepositorySelector(
new org.apache.log4j.spi.DefaultRepositorySelector(
new MyHierarchy(
new org.apache.log4j.spi.RootLogger(
(org.apache.log4j.Level)org.apache.log4j.Level.INFO))), this);
new MyPropertyConfigurator().doConfigure(
pathname, org.apache.log4j.LogManager.getLoggerRepository());
} else {
event.getServletContext().log(
"No log4jConfiguration parameter specified");
}
}
public void contextDestroyed(
ServletContextEvent event
) {
this.context = null;
}
private ServletContext context = null;
}
I had to create a custom Hierarchy since it appears it hardcodes the default logging factory. This version makes sure my factory gets used when the single argument getLogger() method is called (which appears to happen via Logger.getLogger() -> LogManager.getLogger() -> Hierarchy.getLogger()):
MyHierarchy.java
package com.example.myapp.log4j;
import org.apache.log4j.Hierarchy;
import org.apache.log4j.Logger;
import org.apache.log4j.spi.LoggerFactory;
public class MyHierarchy extends Hierarchy
{
public MyHierarchy(Logger root) { super(root); }
#Override
public Logger getLogger(String name) {
return getLogger(name, defaultFactory);
}
private LoggerFactory defaultFactory = new MyLoggerFactory();
}
Not sure I need to customize PropertyConfigurator, but I did in case there is some execution path that actually uses the logger factory instance it keeps a reference to.
MyPropertyConfigurator.java
package com.example.myapp.log4j;
import org.apache.log4j.PropertyConfigurator;
public class MyPropertyConfigurator extends PropertyConfigurator
{
public MyPropertyConfigurator() {
loggerFactory = new MyLoggerFactory();
}
#Override
protected void configureLoggerFactory(java.util.Properties props) {
}
}
The following is my logger factory implementation. The MyEscapedLogger implementation is a subclass of Log4j's Logger class, but it overrides the forcedLog() protected method to escape characters in the message before calling the super version of the method.
MyLoggerFactory.java
package com.example.myapp.log4j;
import org.apache.log4j.spi.LoggerFactory;
public class MyLoggerFactory implements LoggerFactory
{
public Logger makeNewLoggerInstance(String name) {
return new MyEscapedLogger(name);
}
}
After searching for this for quite some time i found this really nice solution:
final LoggerFactory loggerFactory = new LoggerFactory()
{
#Override
public Logger makeNewLoggerInstance(String name)
{
return new MyCustomLogger(name);
// or (if you don't need a custom Logger implementation):
// Logger logger = new Logger(name);
// logger.setSTUFF(...);
// return logger;
}
};
LoggerRepository rep = new Hierarchy(new RootLogger(Level.DEBUG))
{
#Override
public Logger getLogger(String name)
{
return super.getLogger(name, loggerFactory);
}
};
LogManager.setRepositorySelector(new DefaultRepositorySelector(rep), null);
I was using: log4j 1.2.17

How to do a JUnit assert on a message in a logger

I have some code-under-test that calls on a Java logger to report its status.
In the JUnit test code, I would like to verify that the correct log entry was made in this logger. Something along the following lines:
methodUnderTest(bool x){
if(x)
logger.info("x happened")
}
#Test tester(){
// perhaps setup a logger first.
methodUnderTest(true);
assertXXXXXX(loggedLevel(),Level.INFO);
}
I suppose that this could be done with a specially adapted logger (or handler, or formatter), but I would prefer to re-use a solution that already exists. (And, to be honest, it is not clear to me how to get at the logRecord from a logger, but suppose that that's possible.)
I've needed this several times as well. I've put together a small sample below, which you'd want to adjust to your needs. Basically, you create your own Appender and add it to the logger you want. If you'd want to collect everything, the root logger is a good place to start, but you can use a more specific if you'd like. Don't forget to remove the Appender when you're done, otherwise you might create a memory leak. Below I've done it within the test, but setUp or #Before and tearDown or #After might be better places, depending on your needs.
Also, the implementation below collects everything in a List in memory. If you're logging a lot you might consider adding a filter to drop boring entries, or to write the log to a temporary file on disk (Hint: LoggingEvent is Serializable, so you should be able to just serialize the event objects, if your log message is.)
import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.spi.LoggingEvent;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
public class MyTest {
#Test
public void test() {
final TestAppender appender = new TestAppender();
final Logger logger = Logger.getRootLogger();
logger.addAppender(appender);
try {
Logger.getLogger(MyTest.class).info("Test");
}
finally {
logger.removeAppender(appender);
}
final List<LoggingEvent> log = appender.getLog();
final LoggingEvent firstLogEntry = log.get(0);
assertThat(firstLogEntry.getLevel(), is(Level.INFO));
assertThat((String) firstLogEntry.getMessage(), is("Test"));
assertThat(firstLogEntry.getLoggerName(), is("MyTest"));
}
}
class TestAppender extends AppenderSkeleton {
private final List<LoggingEvent> log = new ArrayList<LoggingEvent>();
#Override
public boolean requiresLayout() {
return false;
}
#Override
protected void append(final LoggingEvent loggingEvent) {
log.add(loggingEvent);
}
#Override
public void close() {
}
public List<LoggingEvent> getLog() {
return new ArrayList<LoggingEvent>(log);
}
}
Here is a simple and efficient Logback solution.
It doesn't require to add/create any new class.
It relies on ListAppender : a whitebox logback appender where log entries are added in a public List field that we could so use to make our assertions.
Here is a simple example.
Foo class :
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Foo {
static final Logger LOGGER = LoggerFactory.getLogger(Foo .class);
public void doThat() {
LOGGER.info("start");
//...
LOGGER.info("finish");
}
}
FooTest class :
import org.slf4j.LoggerFactory;
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;
public class FooTest {
#Test
void doThat() throws Exception {
// get Logback Logger
Logger fooLogger = (Logger) LoggerFactory.getLogger(Foo.class);
// create and start a ListAppender
ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
listAppender.start();
// add the appender to the logger
// addAppender is outdated now
fooLogger.addAppender(listAppender);
// call method under test
Foo foo = new Foo();
foo.doThat();
// JUnit assertions
List<ILoggingEvent> logsList = listAppender.list;
assertEquals("start", logsList.get(0)
.getMessage());
assertEquals(Level.INFO, logsList.get(0)
.getLevel());
assertEquals("finish", logsList.get(1)
.getMessage());
assertEquals(Level.INFO, logsList.get(1)
.getLevel());
}
}
JUnit assertions don't sound very adapted to assert some specific properties of the list elements.
Matcher/assertion libraries as AssertJ or Hamcrest appears better for that :
With AssertJ it would be :
import org.assertj.core.api.Assertions;
Assertions.assertThat(listAppender.list)
.extracting(ILoggingEvent::getMessage, ILoggingEvent::getLevel)
.containsExactly(Tuple.tuple("start", Level.INFO), Tuple.tuple("finish", Level.INFO));
Thanks a lot for these (surprisingly) quick and helpful answers; they put me on the right way for my solution.
The codebase were I want to use this, uses java.util.logging as its logger mechanism, and I don't feel at home enough in those codes to completely change that to log4j or to logger interfaces/facades. But based on these suggestions, I 'hacked-up' a j.u.l.handler extension and that works as a treat.
A short summary follows. Extend java.util.logging.Handler:
class LogHandler extends Handler
{
Level lastLevel = Level.FINEST;
public Level checkLevel() {
return lastLevel;
}
public void publish(LogRecord record) {
lastLevel = record.getLevel();
}
public void close(){}
public void flush(){}
}
Obviously, you can store as much as you like/want/need from the LogRecord, or push them all into a stack until you get an overflow.
In the preparation for the junit-test, you create a java.util.logging.Logger and add such a new LogHandler to it:
#Test tester() {
Logger logger = Logger.getLogger("my junit-test logger");
LogHandler handler = new LogHandler();
handler.setLevel(Level.ALL);
logger.setUseParentHandlers(false);
logger.addHandler(handler);
logger.setLevel(Level.ALL);
The call to setUseParentHandlers() is to silence the normal handlers, so that (for this junit-test run) no unnecessary logging happens. Do whatever your code-under-test needs to use this logger, run the test and assertEquality:
libraryUnderTest.setLogger(logger);
methodUnderTest(true); // see original question.
assertEquals("Log level as expected?", Level.INFO, handler.checkLevel() );
}
(Of course, you would move large part of this work into a #Before method and make assorted other improvements, but that would clutter this presentation.)
For Junit 5 (Jupiter) Spring's OutputCaptureExtension is quite useful. Its available since Spring Boot 2.2 and is available in the spring-boot-test artifact.
Example (taken from javadoc):
#ExtendWith(OutputCaptureExtension.class)
class MyTest {
#Test
void test(CapturedOutput output) {
System.out.println("ok");
assertThat(output).contains("ok");
System.err.println("error");
}
#AfterEach
void after(CapturedOutput output) {
assertThat(output.getOut()).contains("ok");
assertThat(output.getErr()).contains("error");
}
}
Effectively you are testing a side-effect of a dependent class. For unit testing you need only to verify that
logger.info()
was called with the correct parameter. Hence use a mocking framework to emulate logger and that will allow you to test your own class's behaviour.
Another option is to mock Appender and verify if message was logged to this appender. Example for Log4j 1.2.x and mockito:
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import org.apache.log4j.Appender;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.spi.LoggingEvent;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
public class MyTest {
private final Appender appender = mock(Appender.class);
private final Logger logger = Logger.getRootLogger();
#Before
public void setup() {
logger.addAppender(appender);
}
#Test
public void test() {
// when
Logger.getLogger(MyTest.class).info("Test");
// then
ArgumentCaptor<LoggingEvent> argument = ArgumentCaptor.forClass(LoggingEvent.class);
verify(appender).doAppend(argument.capture());
assertEquals(Level.INFO, argument.getValue().getLevel());
assertEquals("Test", argument.getValue().getMessage());
assertEquals("MyTest", argument.getValue().getLoggerName());
}
#After
public void cleanup() {
logger.removeAppender(appender);
}
}
I also ran into the same challanged and ended up at this page. Although I am 11 years too late to answers the question, I thought maybe it could be still usefull for others. I found the answer of davidxxx with Logback and the ListAppander very usefull. I used the same configuration for multiple projects, however it was not so fun to copy/paste it and maintaining all the version when I needed to changes something. I thought it would be better to make a library out of it and contribute back to the community. It works with SLFJ4, Log4j, Log4j2, Java Util Logging, JBoss Logging and with Lombok annotations. Please have a look here: LogCaptor for detailed examples and how to add it to your project.
Example situation:
public class FooService {
private static final Logger LOGGER = LoggerFactory.getLogger(FooService.class);
public void sayHello() {
LOGGER.warn("Congratulations, you are pregnant!");
}
}
Example unit test with usage of LogCaptor:
import nl.altindag.log.LogCaptor;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
public class FooServiceTest {
#Test
public void sayHelloShouldLogWarnMessage() {
LogCaptor logCaptor = LogCaptor.forClass(FooService.class);
FooService fooService = new FooService();
fooService.sayHello();
assertThat(logCaptor.getWarnLogs())
.contains("Congratulations, you are pregnant!");
}
}
I wasn't quite sure if I should post this here, because it could also be seen as a way to promote "my library" but I thought it could be helpful for developers who have the same challenges.
Mocking is an option here, although it would be hard, because loggers are generally private static final - so setting a mock logger wouldn't be a piece of cake, or would require modification of the class under test.
You can create a custom Appender (or whatever it's called), and register it - either via a test-only configuration file, or runtime (in a way, dependent on the logging framework).
And then you can get that appender (either statically, if declared in configuration file, or by its current reference, if you are plugging it runtime), and verify its contents.
Inspired by #RonaldBlaschke's solution, I came up with this:
public class Log4JTester extends ExternalResource {
TestAppender appender;
#Override
protected void before() {
appender = new TestAppender();
final Logger rootLogger = Logger.getRootLogger();
rootLogger.addAppender(appender);
}
#Override
protected void after() {
final Logger rootLogger = Logger.getRootLogger();
rootLogger.removeAppender(appender);
}
public void assertLogged(Matcher<String> matcher) {
for(LoggingEvent event : appender.events) {
if(matcher.matches(event.getMessage())) {
return;
}
}
fail("No event matches " + matcher);
}
private static class TestAppender extends AppenderSkeleton {
List<LoggingEvent> events = new ArrayList<LoggingEvent>();
#Override
protected void append(LoggingEvent event) {
events.add(event);
}
#Override
public void close() {
}
#Override
public boolean requiresLayout() {
return false;
}
}
}
... which allows you to do:
#Rule public Log4JTester logTest = new Log4JTester();
#Test
public void testFoo() {
user.setStatus(Status.PREMIUM);
logTest.assertLogged(
stringContains("Note added to account: premium customer"));
}
You could probably make it use hamcrest in a smarter way, but I've left it at this.
For log4j2 the solution is slightly different because AppenderSkeleton is not available anymore. Additionally, using Mockito, or similar library to create an Appender with an ArgumentCaptor will not work if you're expecting multiple logging messages because the MutableLogEvent is reused over multiple log messages. The best solution I found for log4j2 is:
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.Logger;
import org.apache.logging.log4j.core.appender.AbstractAppender;
private static MockedAppender mockedAppender;
private static Logger logger;
#Before
public void setup() {
mockedAppender.message.clear();
}
/**
* For some reason mvn test will not work if this is #Before, but in eclipse it works! As a
* result, we use #BeforeClass.
*/
#BeforeClass
public static void setupClass() {
mockedAppender = new MockedAppender();
logger = (Logger)LogManager.getLogger(MatchingMetricsLogger.class);
logger.addAppender(mockedAppender);
logger.setLevel(Level.INFO);
}
#AfterClass
public static void teardown() {
logger.removeAppender(mockedAppender);
}
#Test
public void test() {
// do something that causes logs
for (String e : mockedAppender.message) {
// add asserts for the log messages
}
}
private static class MockedAppender extends AbstractAppender {
List<String> message = new ArrayList<>();
protected MockedAppender() {
super("MockedAppender", null, null);
}
#Override
public void append(LogEvent event) {
message.add(event.getMessage().getFormattedMessage());
}
}
Wow. I'm unsure why this was so hard. I found I was unable to use any of the code samples above because I was using log4j2 over slf4j. This is my solution:
public class SpecialLogServiceTest {
#Mock
private Appender appender;
#Captor
private ArgumentCaptor<LogEvent> captor;
#InjectMocks
private SpecialLogService specialLogService;
private LoggerConfig loggerConfig;
#Before
public void setUp() {
// prepare the appender so Log4j likes it
when(appender.getName()).thenReturn("MockAppender");
when(appender.isStarted()).thenReturn(true);
when(appender.isStopped()).thenReturn(false);
final LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
final Configuration config = ctx.getConfiguration();
loggerConfig = config.getLoggerConfig("org.example.SpecialLogService");
loggerConfig.addAppender(appender, AuditLogCRUDService.LEVEL_AUDIT, null);
}
#After
public void tearDown() {
loggerConfig.removeAppender("MockAppender");
}
#Test
public void writeLog_shouldCreateCorrectLogMessage() throws Exception {
SpecialLog specialLog = new SpecialLogBuilder().build();
String expectedLog = "this is my log message";
specialLogService.writeLog(specialLog);
verify(appender).append(captor.capture());
assertThat(captor.getAllValues().size(), is(1));
assertThat(captor.getAllValues().get(0).getMessage().toString(), is(expectedLog));
}
}
Here is what i did for logback.
I created a TestAppender class:
public class TestAppender extends AppenderBase<ILoggingEvent> {
private Stack<ILoggingEvent> events = new Stack<ILoggingEvent>();
#Override
protected void append(ILoggingEvent event) {
events.add(event);
}
public void clear() {
events.clear();
}
public ILoggingEvent getLastEvent() {
return events.pop();
}
}
Then in the parent of my testng unit test class I created a method:
protected TestAppender testAppender;
#BeforeClass
public void setupLogsForTesting() {
Logger root = (Logger)LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
testAppender = (TestAppender)root.getAppender("TEST");
if (testAppender != null) {
testAppender.clear();
}
}
I have a logback-test.xml file defined in src/test/resources and I added a test appender:
<appender name="TEST" class="com.intuit.icn.TestAppender">
<encoder>
<pattern>%m%n</pattern>
</encoder>
</appender>
and added this appender to the root appender:
<root>
<level value="error" />
<appender-ref ref="STDOUT" />
<appender-ref ref="TEST" />
</root>
Now in my test classes that extend from my parent test class I can get the appender and get the last message logged and verify the message, the level, the throwable.
ILoggingEvent lastEvent = testAppender.getLastEvent();
assertEquals(lastEvent.getMessage(), "...");
assertEquals(lastEvent.getLevel(), Level.WARN);
assertEquals(lastEvent.getThrowableProxy().getMessage(), "...");
As mentioned from the others you could use a mocking framework. For this to make work you have to expose the logger in your class (although I would propably prefere to make it package private instead of creating a public setter).
The other solution is to create a fake logger by hand. You have to write the fake logger (more fixture code) but in this case I would prefer the enhanced readability of the tests against the saved code from the mocking framework.
I would do something like this:
class FakeLogger implements ILogger {
public List<String> infos = new ArrayList<String>();
public List<String> errors = new ArrayList<String>();
public void info(String message) {
infos.add(message);
}
public void error(String message) {
errors.add(message);
}
}
class TestMyClass {
private MyClass myClass;
private FakeLogger logger;
#Before
public void setUp() throws Exception {
myClass = new MyClass();
logger = new FakeLogger();
myClass.logger = logger;
}
#Test
public void testMyMethod() {
myClass.myMethod(true);
assertEquals(1, logger.infos.size());
}
}
Easiest way
#ExtendWith(OutputCaptureExtension.class)
class MyTestClass {
#Test
void my_test_method(CapturedOutput output) {
assertThat(output).contains("my test log.");
}
}
Here is the sample code to mock log, irrespective of the version used for junit or sping, springboot.
import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.core.Appender;
import org.mockito.ArgumentMatcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.junit.Test;
import static org.mockito.Matchers.argThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class MyTest {
private static Logger logger = LoggerFactory.getLogger(MyTest.class);
#Test
public void testSomething() {
ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME);
final Appender mockAppender = mock(Appender.class);
when(mockAppender.getName()).thenReturn("MOCK");
root.addAppender(mockAppender);
//... do whatever you need to trigger the log
verify(mockAppender).doAppend(argThat(new ArgumentMatcher() {
#Override
public boolean matches(final Object argument) {
return ((LoggingEvent)argument).getFormattedMessage().contains("Hey this is the message I want to see");
}
}));
}
}
As for me you can simplify your test by using JUnit with Mockito.
I propose following solution for it:
import org.apache.log4j.Appender;
import org.apache.log4j.Level;
import org.apache.log4j.LogManager;
import org.apache.log4j.spi.LoggingEvent;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.tuple;
import static org.mockito.Mockito.times;
#RunWith(MockitoJUnitRunner.class)
public class MyLogTest {
private static final String FIRST_MESSAGE = "First message";
private static final String SECOND_MESSAGE = "Second message";
#Mock private Appender appender;
#Captor private ArgumentCaptor<LoggingEvent> captor;
#InjectMocks private MyLog;
#Before
public void setUp() {
LogManager.getRootLogger().addAppender(appender);
}
#After
public void tearDown() {
LogManager.getRootLogger().removeAppender(appender);
}
#Test
public void shouldLogExactlyTwoMessages() {
testedClass.foo();
then(appender).should(times(2)).doAppend(captor.capture());
List<LoggingEvent> loggingEvents = captor.getAllValues();
assertThat(loggingEvents).extracting("level", "renderedMessage").containsExactly(
tuple(Level.INFO, FIRST_MESSAGE)
tuple(Level.INFO, SECOND_MESSAGE)
);
}
}
That's why we have nice flexibility for tests with different message quantity
Note that in Log4J 2.x, the public interface org.apache.logging.log4j.Logger doesn't include the setAppender() and removeAppender() methods.
But if you're not doing anything too fancy, you should be able to cast it to the implementation class org.apache.logging.log4j.core.Logger, which does expose those methods.
Here's an example with Mockito and AssertJ:
// Import the implementation class rather than the API interface
import org.apache.logging.log4j.core.Logger;
// Cast logger to implementation class to get access to setAppender/removeAppender
Logger log = (Logger) LogManager.getLogger(MyClassUnderTest.class);
// Set up the mock appender, stubbing some methods Log4J needs internally
Appender appender = mock(Appender.class);
when(appender.getName()).thenReturn("Mock Appender");
when(appender.isStarted()).thenReturn(true);
log.addAppender(appender);
try {
new MyClassUnderTest().doSomethingThatShouldLogAnError();
} finally {
log.removeAppender(appender);
}
// Verify that we got an error with the expected message
ArgumentCaptor<LogEvent> logEventCaptor = ArgumentCaptor.forClass(LogEvent.class);
verify(appender).append(logEventCaptor.capture());
LogEvent logEvent = logEventCaptor.getValue();
assertThat(logEvent.getLevel()).isEqualTo(Level.ERROR);
assertThat(logEvent.getMessage().getFormattedMessage()).contains(expectedErrorMessage);
You don't need to rely on hardcoded static global Loggers in your class implementation, you can provide a default logger in the default constructor and then use a specific constructor to set a reference to the provided logger.
class MyClassToTest {
private final Logger logger;
public MyClassToTest() {
this(SomeStatic.logger);
};
MyClassToTest(Logger logger) {
this.logger = logger;
};
public void someOperation() {
logger.warn("warning message");
// ...
};
};
class MyClassToTestTest {
#Test
public warnCalled() {
Logger loggerMock = mock(Logger.class);
MyClassTest myClassToTest = new MyClassToTest(logger);
myClassToTest.someOperation();
verify(loggerMock).warn(anyString());
};
}
Check this library https://github.com/Hakky54/log-captor
Include in your maven file the reference for the library:
<dependency>
<groupId>io.github.hakky54</groupId>
<artifactId>logcaptor</artifactId>
<version>2.5.0</version>
<scope>test</scope>
</dependency>
In java code test method you should include this:
LogCaptor logCaptor = LogCaptor.forClass(MyClass.class);
// do the test logic....
assertThat(logCaptor.getLogs()).contains("Some log to assert");
Here is a nice and elegant way to approach this problem:
https://www.baeldung.com/junit-asserting-logs
What I have done if all I want to do is see that some string was logged (as opposed to verifying exact log statements which is just too brittle) is to redirect StdOut to a buffer, do a contains, then reset StdOut:
PrintStream original = System.out;
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
System.setOut(new PrintStream(buffer));
// Do something that logs
assertTrue(buffer.toString().contains(myMessage));
System.setOut(original);
The API for Log4J2 is slightly different. Also you might be using its async appender. I created a latched appender for this:
public static class LatchedAppender extends AbstractAppender implements AutoCloseable {
private final List<LogEvent> messages = new ArrayList<>();
private final CountDownLatch latch;
private final LoggerConfig loggerConfig;
public LatchedAppender(Class<?> classThatLogs, int expectedMessages) {
this(classThatLogs, null, null, expectedMessages);
}
public LatchedAppender(Class<?> classThatLogs, Filter filter, Layout<? extends Serializable> layout, int expectedMessages) {
super(classThatLogs.getName()+"."+"LatchedAppender", filter, layout);
latch = new CountDownLatch(expectedMessages);
final LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
final Configuration config = ctx.getConfiguration();
loggerConfig = config.getLoggerConfig(LogManager.getLogger(classThatLogs).getName());
loggerConfig.addAppender(this, Level.ALL, ThresholdFilter.createFilter(Level.ALL, null, null));
start();
}
#Override
public void append(LogEvent event) {
messages.add(event);
latch.countDown();
}
public List<LogEvent> awaitMessages() throws InterruptedException {
assertTrue(latch.await(10, TimeUnit.SECONDS));
return messages;
}
#Override
public void close() {
stop();
loggerConfig.removeAppender(this.getName());
}
}
Use it like this:
try (LatchedAppender appender = new LatchedAppender(ClassUnderTest.class, 1)) {
ClassUnderTest.methodThatLogs();
List<LogEvent> events = appender.awaitMessages();
assertEquals(1, events.size());
//more assertions here
}//appender removed
Another idea worth mentioning, although it's an older topic, is creating a CDI producer to inject your logger so the mocking becomes easy. (And it also gives the advantage of not having to declare the "whole logger statement" anymore, but that's off-topic)
Example:
Creating the logger to inject:
public class CdiResources {
#Produces #LoggerType
public Logger createLogger(final InjectionPoint ip) {
return Logger.getLogger(ip.getMember().getDeclaringClass());
}
}
The qualifier:
#Qualifier
#Retention(RetentionPolicy.RUNTIME)
#Target({TYPE, METHOD, FIELD, PARAMETER})
public #interface LoggerType {
}
Using the logger in your production code:
public class ProductionCode {
#Inject
#LoggerType
private Logger logger;
public void logSomething() {
logger.info("something");
}
}
Testing the logger in your test code (giving an easyMock example):
#TestSubject
private ProductionCode productionCode = new ProductionCode();
#Mock
private Logger logger;
#Test
public void testTheLogger() {
logger.info("something");
replayAll();
productionCode.logSomething();
}
Using Jmockit (1.21) I was able to write this simple test.
The test makes sure a specific ERROR message is called just once.
#Test
public void testErrorMessage() {
final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger( MyConfig.class );
new Expectations(logger) {{
//make sure this error is happens just once.
logger.error( "Something went wrong..." );
times = 1;
}};
new MyTestObject().runSomethingWrong( "aaa" ); //SUT that eventually cause the error in the log.
}
Mocking the Appender can help capture the log lines.
Find sample on: http://clearqa.blogspot.co.uk/2016/12/test-log-lines.html
// Fully working test at: https://github.com/njaiswal/logLineTester/blob/master/src/test/java/com/nj/Utils/UtilsTest.java
#Test
public void testUtilsLog() throws InterruptedException {
Logger utilsLogger = (Logger) LoggerFactory.getLogger("com.nj.utils");
final Appender mockAppender = mock(Appender.class);
when(mockAppender.getName()).thenReturn("MOCK");
utilsLogger.addAppender(mockAppender);
final List<String> capturedLogs = Collections.synchronizedList(new ArrayList<>());
final CountDownLatch latch = new CountDownLatch(3);
//Capture logs
doAnswer((invocation) -> {
LoggingEvent loggingEvent = invocation.getArgumentAt(0, LoggingEvent.class);
capturedLogs.add(loggingEvent.getFormattedMessage());
latch.countDown();
return null;
}).when(mockAppender).doAppend(any());
//Call method which will do logging to be tested
Application.main(null);
//Wait 5 seconds for latch to be true. That means 3 log lines were logged
assertThat(latch.await(5L, TimeUnit.SECONDS), is(true));
//Now assert the captured logs
assertThat(capturedLogs, hasItem(containsString("One")));
assertThat(capturedLogs, hasItem(containsString("Two")));
assertThat(capturedLogs, hasItem(containsString("Three")));
}
Use the below code. I am using same code for my spring integration test where I am using log back for logging. Use method assertJobIsScheduled to assert the text printed in the log.
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.core.Appender;
private Logger rootLogger;
final Appender mockAppender = mock(Appender.class);
#Before
public void setUp() throws Exception {
initMocks(this);
when(mockAppender.getName()).thenReturn("MOCK");
rootLogger = (Logger) LoggerFactory.getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME);
rootLogger.addAppender(mockAppender);
}
private void assertJobIsScheduled(final String matcherText) {
verify(mockAppender).doAppend(argThat(new ArgumentMatcher() {
#Override
public boolean matches(final Object argument) {
return ((LoggingEvent)argument).getFormattedMessage().contains(matcherText);
}
}));
}
There are two things that you might be trying to test.
When there is an event of interest to the operator of my program, does my program perform an appropriate logging operation, which can inform the operator of that event.
When my program performs a logging operation, does the log message it produces have the correct text.
Those two things are actually different things, and so could be tested separately. However, testing the second (the text of messages) is so problematic, I recommend against doing it at all. A test of a message text will ultimately consist of checking that one text string (the expected message text) is the same as, or can be trivially derived from, the text string used in your logging code.
Those tests do not test program logic at all, they only test that one resource (a string) is equivalent to another resource.
The tests are fragile; even a minor tweak to the formatting of a log message breaks your tests.
The tests are incompatible with internationalisation (translation) of your logging interface.The tests assume there is only one possible message text, and thus only one possible human language.
Note that having your program code (implementing some business logic, perhaps) directly calling the text logging interface is poor design (but unfortunately very commom). Code that is responsible for business logic is also deciding some logging policy and the text of log messages. It mixes business logic with user interface code (yes, log messages are part of your program's user interface). Those things should be separate.
I therefore recommend that business logic does not directly generate the text of log messages. Instead have it delegate to a logging object.
The class of the logging object should provide a suitable internal API, which your business object can use to express the event that has occurred using objects of your domain model, not text strings.
The implementation of your logging class is responsible for producing text representations of those domain objects, and rendering a suitable text description of the event, then forwarding that text message to the low level logging framework (such as JUL, log4j or slf4j).
Your business logic is responsible only for calling the correct methods of the internal API of your logger class, passing the correct domain objects, to describe the actual events that occurred.
Your concrete logging class implements an interface, which describes the internal API your business logic may use.
Your class(es) that implements business logic and must perform logging has a reference to the logging object to delegate to. The class of the reference is the abstract interface.
Use dependency injection to set up the reference to the logger.
You can then test that your business logic classes correctly tell the logging interface about events, by creating a mock logger, which implements the internal logging API, and using dependency injection in the set up phase of your test.
Like this:
public class MyService {// The class we want to test
private final MyLogger logger;
public MyService(MyLogger logger) {
this.logger = Objects.requireNonNull(logger);
}
public void performTwiddleOperation(Foo foo) {// The method we want to test
...// The business logic
logger.performedTwiddleOperation(foo);
}
};
public interface MyLogger {
public void performedTwiddleOperation(Foo foo);
...
};
public final class MySl4jLogger: implements MyLogger {
...
#Override
public void performedTwiddleOperation(Foo foo) {
logger.info("twiddled foo " + foo.getId());
}
}
public final void MyProgram {
public static void main(String[] argv) {
...
MyLogger logger = new MySl4jLogger(...);
MyService service = new MyService(logger);
startService(service);// or whatever you must do
...
}
}
public class MyServiceTest {
...
static final class MyMockLogger: implements MyLogger {
private Food.id id;
private int nCallsPerformedTwiddleOperation;
...
#Override
public void performedTwiddleOperation(Foo foo) {
id = foo.id;
++nCallsPerformedTwiddleOperation;
}
void assertCalledPerformedTwiddleOperation(Foo.id id) {
assertEquals("Called performedTwiddleOperation", 1, nCallsPerformedTwiddleOperation);
assertEquals("Called performedTwiddleOperation with correct ID", id, this.id);
}
};
#Test
public void testPerformTwiddleOperation_1() {
// Setup
MyMockLogger logger = new MyMockLogger();
MyService service = new MyService(logger);
Foo.Id id = new Foo.Id(...);
Foo foo = new Foo(id, 1);
// Execute
service.performedTwiddleOperation(foo);
// Verify
...
logger.assertCalledPerformedTwiddleOperation(id);
}
}
I answered a similar question for log4j see how-can-i-test-with-junit-that-a-warning-was-logged-with-log4
This is newer and example with Log4j2 (tested with 2.11.2) and junit 5;
package com.whatever.log;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.Logger;
import org.apache.logging.log4j.core.*;
import org.apache.logging.log4j.core.appender.AbstractAppender;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.LoggerConfig;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginElement;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.*;
class TestLogger {
private TestAppender testAppender;
private LoggerConfig loggerConfig;
private final Logger logger = (Logger)
LogManager.getLogger(ClassUnderTest.class);
#Test
#DisplayName("Test Log Junit5 and log4j2")
void test() {
ClassUnderTest.logMessage();
final LogEvent loggingEvent = testAppender.events.get(0);
//asset equals 1 because log level is info, change it to debug and
//the test will fail
assertTrue(testAppender.events.size()==1,"Unexpected empty log");
assertEquals(Level.INFO,loggingEvent.getLevel(),"Unexpected log level");
assertEquals(loggingEvent.getMessage().toString()
,"Hello Test","Unexpected log message");
}
#BeforeEach
private void setup() {
testAppender = new TestAppender("TestAppender", null);
final LoggerContext context = logger.getContext();
final Configuration configuration = context.getConfiguration();
loggerConfig = configuration.getLoggerConfig(logger.getName());
loggerConfig.setLevel(Level.INFO);
loggerConfig.addAppender(testAppender,Level.INFO,null);
testAppender.start();
context.updateLoggers();
}
#AfterEach
void after(){
testAppender.stop();
loggerConfig.removeAppender("TestAppender");
final LoggerContext context = logger.getContext();
context.updateLoggers();
}
#Plugin( name = "TestAppender", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE)
static class TestAppender extends AbstractAppender {
List<LogEvent> events = new ArrayList();
protected TestAppender(String name, Filter filter) {
super(name, filter, null);
}
#PluginFactory
public static TestAppender createAppender(
#PluginAttribute("name") String name,
#PluginElement("Filter") Filter filter) {
return new TestAppender(name, filter);
}
#Override
public void append(LogEvent event) {
events.add(event);
}
}
static class ClassUnderTest {
private static final Logger LOGGER = (Logger) LogManager.getLogger(ClassUnderTest.class);
public static void logMessage(){
LOGGER.info("Hello Test");
LOGGER.debug("Hello Test");
}
}
}
Using the following maven dependencies
<dependency>
<artifactId>log4j-core</artifactId>
<packaging>jar</packaging>
<version>2.11.2</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.5.0</version>
<scope>test</scope>
</dependency>
In my case I solved the same issue as bellow:
Logger root = (Logger) LoggerFactory.getLogger(CSVTasklet.class); //CSVTasklet is my target class
final Appender mockAppender = mock(Appender.class);
root.addAppender(mockAppender);
verify(mockAppender).doAppend(argThat((ArgumentMatcher) argument -> ((LoggingEvent) argument).getMessage().contains("No projects."))); // I checked "No projects." in the log
Unit testing by adding an Appender does not really test the Logger's configuration. So, I think that it's one of the unique cases in which unit tests do not bring that much value, but an integration test brings a lot of value (especially if your logging has some auditing purposes).
In order to create an integration test for it, let us suppose that you are running with a simple ConsoleAppender and want to test its output. Then, you should test how the message is written to its own ByteArrayOutputStream from System.out.
In that sense, I would do the following (I'm using JUnit 5):
public class Slf4jAuditLoggerTest {
private final ByteArrayOutputStream outContent = new ByteArrayOutputStream();
#BeforeEach
public void beforeEach() {
System.setOut(new PrintStream(outContent));
}
In that way, you are able to test its output by simply:
#Test
public void myTest() {
// Given...
// When...
// Then
assertTrue(outContent.toString().contains("[INFO] My formatted string from Logger"));
}
If you do so, you will bring much more value to your project and will not need to using an in-memory implementation, create a new Appender, or whatsoever.

Categories