Accessing private static members (e.g. loggers) from AspectJ - java

Suppose that I have this class
public class MyClass {
private Logger log = LogFactory.getLogger(MyClass.class);
public void doSomething() {
// doing something
}
}
Suppose that I want to write an aspect to log entry and exit:
public aspect TraceAspect {
pointcut method(): execution(* *(..));
before(): method(){
log.info("entering method");
}
after(): method(){
log.info("existing method");
}
}
The problem here is that I want to access the log object inside the class with aspect, but I don't know how. I don't want to create a new logger because I want to keep all the data associated with the class logger when logging. Is there a way or a pattern to access class data?
EDIT: wanted to state that this aspect should trace all classes that have log field. That is, I might have many classes: MyClass, MyClass1, YourClass2, RepositoryClass, etc.

What NĂ¡ndor said is technically correct, but my advice is: Please avoid accessing private members or methods whenever you can because they are private for a reason. For instance, they are subject to change even when the public interface of a class does not change. So you can never rely on their existence or their naming. Furthermore, a cross-cutting concern should also comply with design principles like encapsulation as much as possible.
This specific case is about Slf4J loggers, more precisely about your wish to avoid creating redundant logger objects. Well, Slf4J is not as stupid or careless as you might think concerning object creation. All classes implementing ILoggerFactory use an internal map in order to cache existing loggers for given (class) names, see e.g.
SimpleLoggerFactory
JDK14LoggerFactory
So why don't you just relax and access the corresponding loggers from your aspect using the target class names. This even works if a target class does not have its own static logger:
Application classes:
package de.scrum_master.app;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MyClassWithLogger {
private Logger log = LoggerFactory.getLogger(MyClassWithLogger.class);
public void doSomething() {}
}
package de.scrum_master.app;
public class MyClassWithoutLogger {
public void doSomething() {}
}
package de.scrum_master.app;
public class Application {
public static void main(String[] args) {
new MyClassWithLogger().doSomething();
new MyClassWithoutLogger().doSomething();
}
}
Aspect:
package de.scrum_master.aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public aspect TraceAspect {
pointcut executedMethods(Object targetObject) :
execution(!static * *(..)) && target(targetObject);
before(Object targetObject) : executedMethods(targetObject) {
Logger log = LoggerFactory.getLogger(targetObject.getClass());
log.info("Entering " + thisJoinPoint);
}
after(Object targetObject) : executedMethods(targetObject) {
Logger log = LoggerFactory.getLogger(targetObject.getClass());
log.info("Exiting " + thisJoinPoint);
}
}
Console log with Slf4J configured to use simple logger:
[main] INFO de.scrum_master.app.MyClassWithLogger - Entering execution(void de.scrum_master.app.MyClassWithLogger.doSomething())
[main] INFO de.scrum_master.app.MyClassWithLogger - Exiting execution(void de.scrum_master.app.MyClassWithLogger.doSomething())
[main] INFO de.scrum_master.app.MyClassWithoutLogger - Entering execution(void de.scrum_master.app.MyClassWithoutLogger.doSomething())
[main] INFO de.scrum_master.app.MyClassWithoutLogger - Exiting execution(void de.scrum_master.app.MyClassWithoutLogger.doSomething())

Related

AOP - accessing a protected/private attribute from the intercepted class

I am working on a project that is basically a lot of processes that run periodically. Each process is a different class that extends an abstract class RunnableProcess we created, which contains a private attribute Map<String, String> result and the abstract method run with the signature below:
public abstract void run(Map processContext) throws IOException;
To improve modularization on the project, I'm starting to use Aspect Oriented Programming (AOP) to intercept the run calls from every RunnableProcess. I am still learning AOP, and I have the following code until now:
import static org.slf4j.LoggerFactory.getLogger;
import org.slf4j.Logger;
import process.RunnableProcess;
import java.util.Map;
public aspect ProcessRunInterceptor {
private Logger logger;
pointcut runProcess() : call(void RunnableProcess.run(Map));
after(): runProcess() {
logger = getLogger(thisJoinPoint.getClass());
logger.info("process run successfully");
}
}
It is working, but I want to log more information than just "process run successfully". I have this information inside the intercepted class, in the result attribute mentioned above. Is it possible to access it in the advice without changing the implementation of RunnableProcess?
I can (prefer not to, but if it would be the only choice...) change the attribute from private to protected, but I wouldn't change it to public. I also would not like to create a get method for it.
Building upon my answer to your other question, I will explain to you what you can do. A few hints:
Instead of before() and after() you could just use around().
You should not use a private member for the process instance in a singleton aspect because if you have asynchronous processes in multiple threads, the member could be overwritten. Thus, your approach is not thread-safe and you should use a local variable instead.
You should not print "process run successfully" in an after() advice because after() also runs after an exception. So you cannot safely assume that the process ran successfully, only that it ran at all. You should rather write "finished process" or similar. BTW, if you want to differentiate between successful processes and such ending with an exception, you might want to look into pointcut types after() returning and after() throwing().
It does not make sense to use an abstract base class and not define the member result there directly. Why add it as a private member in each subclass if you can have it as a protected member in the parent? We are still doing OOP here (beside AOP, of course), right? The advantage is that you can access the member directly from the aspect using the base class like you already do in your pointcut.
Here is an MCVE for you:
Process classes:
package de.scrum_master.app;
import java.io.IOException;
import java.util.Map;
public abstract class RunnableProcess {
protected String result = "foo";
public abstract void run(Map processContext) throws IOException;
}
package de.scrum_master.app;
import java.io.IOException;
import java.util.Map;
public class FirstRunnableProcess extends RunnableProcess {
#Override
public void run(Map processContext) throws IOException {
System.out.println("I am #1");
result = "first";
}
}
package de.scrum_master.app;
import java.io.IOException;
import java.util.Map;
public class SecondRunnableProcess extends RunnableProcess {
#Override
public void run(Map processContext) throws IOException {
System.out.println("I am #2");
result = "second";
}
}
Driver application:
package de.scrum_master.app;
import java.io.IOException;
public class Application {
public static void main(String[] args) throws IOException {
new FirstRunnableProcess().run(null);
new SecondRunnableProcess().run(null);
}
}
Aspect:
Here you just bind the target() object to a parameter in the pointcut and use it in both advices.
package de.scrum_master.aspect;
import static org.slf4j.LoggerFactory.getLogger;
import org.slf4j.Logger;
import de.scrum_master.app.RunnableProcess;
import java.util.Map;
public privileged aspect ProcessRunInterceptorProtocol {
pointcut runProcess(RunnableProcess process) :
call(void RunnableProcess.run(Map)) && target(process);
before(RunnableProcess process): runProcess(process) {
Logger logger = getLogger(process.getClass());
logger.info("logger = " + logger);
logger.info("running process = " + thisJoinPoint);
}
after(RunnableProcess process): runProcess(process) {
Logger logger = getLogger(process.getClass());
logger.info("finished process = " + thisJoinPoint);
logger.info("result = " + process.result);
}
}
Console log with JDK logging (some noise removed):
INFORMATION: logger = org.slf4j.impl.JDK14LoggerAdapter(de.scrum_master.app.FirstRunnableProcess)
INFORMATION: running process = call(void de.scrum_master.app.FirstRunnableProcess.run(Map))
I am #1
INFORMATION: finished process = call(void de.scrum_master.app.FirstRunnableProcess.run(Map))
INFORMATION: result = first
INFORMATION: logger = org.slf4j.impl.JDK14LoggerAdapter(de.scrum_master.app.SecondRunnableProcess)
INFORMATION: running process = call(void de.scrum_master.app.SecondRunnableProcess.run(Map))
I am #2
INFORMATION: finished process = call(void de.scrum_master.app.SecondRunnableProcess.run(Map))
INFORMATION: result = second

Get class name and method parameters in the aspect

I am working on a project that is basically a lot of processes that run periodically. Each process is a different class that extends an abstract class RunnableProcess we created, which contains the abstract method run with the signature below:
public abstract void run(Map processContext) throws IOException;
To improve modularization on the project, I'm starting to use Aspect Oriented Programming (AOP) to intercept the run calls from every RunnableProcess. I am still learning AOP, and I have the following code until now:
import static org.slf4j.LoggerFactory.getLogger;
import org.slf4j.Logger;
import process.RunnableProcess;
import java.util.Map;
public aspect ProcessRunInterceptorProtocol {
pointcut runProcess() : call(void RunnableProcess.run(Map));
before(): runProcess() {
logger = getLogger(getClass());
logger.info("running process " + thisJoinPoint);
}
after(): runProcess() {
logger = getLogger(getClass());
logger.info("process run successfuly " + thisJoinPoint);
}
private Logger logger;
}
The problem I'm having is related to the logger (org.slf4j.Logger) initialization - I would like it to be linked with the process class (the one that extended RunnableProcess, and it is being intercepted by the aspect), which is not happening here (the getClass() retrieves the aspect class). How can I do that without changing the implementation of RunnableProcess and its childs?
You want the target object on which the method is executed. Try this:
logger = getLogger(thisJoinPoint.getTarget().getClass());

AspectJ designator #args() not working in Spring AOP

I am learning Spring and I searched a lot about how to properly use #args() AspectJ designator but I am still not clear completely. What I know about it is that it limits joint-point matches to the execution of methods whose arguments are annoted with the given annotation types. This does not seem to work in my case.
So here goes my files:
Human.java
#Component
public class Human {
int sleepHours;
public int sleep(String sleepHours) {
this.sleepHours = Integer.parseInt(sleepHours);
System.out.println("Humans sleep for " + this.sleepHours + " hours.");
return this.sleepHours+1;
}
}
Sleepable.java - Sleepable annotation
package com.aspect;
public #interface Sleepable {
}
SleepingAspect.java - Aspect
#Component
#Aspect
public class SleepingAspect {
#Pointcut("#args(com.aspect.Sleepable)")
public void sleep(){};
#Before("sleep()")
public void beforeSleep() {
System.out.println("Closing eyes before sleeping");
}
#AfterReturning("sleep()")
public void afterSleep() {
System.out.println("Opening eyes after sleep");
}
}
MainApp.java
public class MainApp {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
Human human = (Human) context.getBean("human");
#Sleepable
String sleepHours = "8";
human.sleep(sleepHours);
}
}
Output
Humans sleep for 8 hours.
Expected Output
Closing eyes before sleeping
Humans sleep for 8 hours.
Opening eyes after sleep
You have several mistakes in your code:
Spring AOP can only intercept method calls upon Spring components. What you are trying to intercept is an annotation on a local variable. Not even the much more powerful AspectJ can intercept anything concerning local variables, only read/write access to class members. Thus, what you are trying to do is impossible. And by the way, it is bad application design. Why would anyone want to rely on method internals when trying to apply cross-cutting behaviour? Method internals are subject to frequent refactoring. Suggestion: Put your annotation on method public int sleep(String sleepHours).
Your annotation is invisible during runtime because your forgot to add a meta annotation like #Retention(RetentionPolicy.RUNTIME).
#args is the wrong pointcut type. It captures method arguments the types of which are annotated. You want to use #annotation(com.aspect.Sleepable) instead.
I think you should not try a copy & paste cold start with Spring AOP but read the Spring AOP manual first. Everything I explained here can be found there.
Update: So according to you comments you were just practicing and trying to make up an example for #args(). Here is one in plain AspectJ. You can easily use it in similar form in Spring AOP. The #Before advice shows you how to match on an argument with an annotation on its class, the #After advice also shows how to bind the corresponding annotation to an advice argument.
Annotation + class using it:
package com.company.app;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
#Retention(RetentionPolicy.RUNTIME)
public #interface MyAnnotation {}
package com.company.app;
#MyAnnotation
public class MyClass {}
Driver application:
package com.company.app;
public class Application {
public static void main(String[] args) {
new Application().doSomething(new MyClass(), 11);
}
public String doSomething(MyClass myClass, int i) {
return "blah";
}
}
As you can see, here we use the annotated class MyClass in a method argument. This can be matched with #args() in the following aspect.
Aspect:
package com.company.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import com.company.app.MyAnnotation;
#Aspect
public class MyAspect {
#Before("#args(com.company.app.MyAnnotation, ..)")
public void myBeforeAdvice(JoinPoint thisJoinPoint) {
System.out.println("Before " + thisJoinPoint);
}
#After("#args(myAnnotation, ..)")
public void myAfterAdvice(JoinPoint thisJoinPoint, MyAnnotation myAnnotation) {
System.out.println("After " + thisJoinPoint + " -> " + myAnnotation);
}
}
Console log:
Before call(String com.company.app.Application.doSomething(MyClass, int))
Before execution(String com.company.app.Application.doSomething(MyClass, int))
After execution(String com.company.app.Application.doSomething(MyClass, int)) -> #com.company.app.MyAnnotation()
After call(String com.company.app.Application.doSomething(MyClass, int)) -> #com.company.app.MyAnnotation()
Of course, call() joinpoints are unavailable in Spring AOP, so there you would only see two lines of log output.

Is it possible to handle all instances of a running Loggers during particular time period

Is it possible to handle all running Logger classes and copy their output to some another hub during some timeperiod?
For example:
package com.ubs.testing;
import org.apache.log4j.Logger;
public class ClassA {
private static final Logger LOG = Logger.getLogger(ClassA.class);
public void classAMethod() {
LOG.info("Class A starts logging");
}
}
package com.ubs.testing;
import org.apache.log4j.Logger;
public class ClassB {
private static final Logger LOG = Logger.getLogger(ClassB.class);
public void classBMethod() {
LOG.info("Class B starts logging");
}
}
package com.ubs.testing;
import org.apache.log4j.Logger;
import org.testng.annotations.Test;
public class ClassABTest {
private static final Logger LOG = Logger.getLogger(ClassABTest.class);
#Test
public void testClassAB() throws Exception {
LOG.info("Test class AB starts");
/*Some logic*/
/* >>>HERE I WANT TO START HANDLING OF THE LOGGING OUTPUT AND STORE IT IN SOME OTHER OBJECT<<<*/
ClassA a = new ClassA();
ClassB b = new ClassB();
a.classAMethod(); // I want to get "Class A starts logging" message from ClassA Logger here
b.classBMethod(); // I want to get "Class B starts logging" message from ClassA Logger here
/* >>>HERE I WANT TO FINISH HANDLING OF THE LOGGING OUTPUT IN SOME OTHER OBJECT<<<*/
LOG.info("Test class AB ends");
}
}
Sorry if similar questions were posted before.
Thanks in advance for suggestions.
Forgot to left respone erlier.
Just switching from log4j to logback gave me a possibility to left markers, which can be handled by custom appender.
Another solution can be realized using Spring aop. Unfortunatelly mentioned issue cannot be resolved in this way, due to absence of the Spring framework within a project.

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