How to intercept SLF4J (with logback) logging via a JUnit test? - java

Is it possible to somehow intercept the logging (SLF4J + logback) and get an InputStream (or something else that is readable) via a JUnit test case...?

The Slf4j API doesn't provide such a way but Logback provides a simple solution.
You can use ListAppender : a whitebox logback appender where log entries are added in a public List field that we could 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
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());
}
}
You can also use Matcher/assertion libraries as AssertJ or Hamcrest.
With AssertJ it would be :
import org.assertj.core.api.Assertions;
Assertions.assertThat(listAppender.list)
.extracting(ILoggingEvent::getFormattedMessage, ILoggingEvent::getLevel)
.containsExactly(Tuple.tuple("start", Level.INFO), Tuple.tuple("finish", Level.INFO));

You can create a custom appender
public class TestAppender extends AppenderBase<LoggingEvent> {
static List<LoggingEvent> events = new ArrayList<>();
#Override
protected void append(LoggingEvent e) {
events.add(e);
}
}
and configure logback-test.xml to use it. Now we can check logging events from our test:
#Test
public void test() {
...
Assert.assertEquals(1, TestAppender.events.size());
...
}
NOTE: Use ILoggingEvent if you do not get any output - see the comment section for the reasoning.

With JUnit5
private ListAppender<ILoggingEvent> logWatcher;
#BeforeEach
void setup() {
logWatcher = new ListAppender<>();
logWatcher.start();
((Logger) LoggerFactory.getLogger(MyClass.class)).addAppender(logWatcher);
}
Note: MyClass.class - should be your Prod class, you expect the log output from
use: (AssertJ example)
#Test
void myMethod_logs2Messages() {
...
int logSize = logWatcher.list.size();
assertThat(logWatcher.list.get(logSize - 2).getFormattedMessage()).contains("EXPECTED MSG 1");
assertThat(logWatcher.list.get(logSize - 1).getFormattedMessage()).contains("EXPECTED MSG 2");
}
destroy:
Detach is recommended, for a better performance:
#AfterEach
void teardown() {
((Logger) LoggerFactory.getLogger(MyClass.class)).detachAndStopAllAppenders();
}
imports:
import org.slf4j.LoggerFactory;
import ch.qos.logback.core.read.ListAppender;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.Logger;
credits to: #davidxxx's answer. See it for import ch.qos.logback... details: https://stackoverflow.com/a/52229629/601844

You can use slf4j-test from http://projects.lidalia.org.uk/slf4j-test/.
It replaces the entire logback slf4j implementation by it's own slf4j api implementation for tests and provides an api to assert against logging events.
example:
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<classpathDependencyExcludes>
<classpathDependencyExcludes>ch.qos.logback:logback-classic</classpathDependencyExcludes>
</classpathDependencyExcludes>
</configuration>
</plugin>
</plugins>
</build>
public class Slf4jUser {
private static final Logger logger = LoggerFactory.getLogger(Slf4jUser.class);
public void aMethodThatLogs() {
logger.info("Hello World!");
}
}
public class Slf4jUserTest {
Slf4jUser slf4jUser = new Slf4jUser();
TestLogger logger = TestLoggerFactory.getTestLogger(Slf4jUser.class);
#Test
public void aMethodThatLogsLogsAsExpected() {
slf4jUser.aMethodThatLogs();
assertThat(logger.getLoggingEvents(), is(asList(info("Hello World!"))));
}
#After
public void clearLoggers() {
TestLoggerFactory.clear();
}
}

A simple solution could be to mock the appender with Mockito (for example)
MyClass.java
#Slf4j
class MyClass {
public void doSomething() {
log.info("I'm on it!");
}
}
MyClassTest.java
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.mockito.Mockito.verify;
#RunWith(MockitoJUnitRunner.class)
public class MyClassTest {
#Mock private Appender<ILoggingEvent> mockAppender;
private MyClass sut = new MyClass();
#Before
public void setUp() {
Logger logger = (Logger) LoggerFactory.getLogger(MyClass.class.getName());
logger.addAppender(mockAppender);
}
#Test
public void shouldLogInCaseOfError() {
sut.doSomething();
verify(mockAppender).doAppend(ArgumentMatchers.argThat(argument -> {
assertThat(argument.getMessage(), containsString("I'm on it!"));
assertThat(argument.getLevel(), is(Level.INFO));
return true;
}));
}
}
NOTE: I'm using assertion rather than returning false as it makes code and (possible) error easier to read, but it won't work if you have multiple verifications. In that case you need to return boolean indicating if the value is as expected.

Although creating a custom logback appender is a good solution, it is only the first step, you will eventually end up developing/reinventing slf4j-test, and if you go a bit further: spf4j-slf4j-test or other frameworks that I don't know of yet.
You will eventually need to worry about how many events you keep in memory, fail unit tests when a error is logged (and not asserted), make debug logs available on test failure, etc...
Disclaimer: I am the author of spf4j-slf4j-test, I wrote this backend to be able to better test spf4j, which is a good place to look at for examples on how to use spf4j-slf4j-test. One of the main advantages I achieved was reducing my build output (which is limited with Travis), while still having all the detail I need when failure happens.

I would recommend a simple, reusable spy implementation that can be included in a test as JUnit rule:
public final class LogSpy extends ExternalResource {
private Logger logger;
private ListAppender<ILoggingEvent> appender;
#Override
protected void before() {
appender = new ListAppender<>();
logger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); // cast from facade (SLF4J) to implementation class (logback)
logger.addAppender(appender);
appender.start();
}
#Override
protected void after() {
logger.detachAppender(appender);
}
public List<ILoggingEvent> getEvents() {
if (appender == null) {
throw new UnexpectedTestError("LogSpy needs to be annotated with #Rule");
}
return appender.list;
}
}
In your test, you'd activate the spy in the following way:
#Rule
public LogSpy log = new LogSpy();
Call log.getEvents() (or other, custom methods) to check the logged events.

I had problems when testing logs line like: LOGGER.error(message, exception).
The solution described in http://projects.lidalia.org.uk/slf4j-test/ tries to assert as well on the exception and it is not easy (and in my opinion worthless) to recreate the stacktrace.
I resolved in this way:
import org.junit.Test;
import org.slf4j.Logger;
import uk.org.lidalia.slf4jext.LoggerFactory;
import uk.org.lidalia.slf4jtest.TestLogger;
import uk.org.lidalia.slf4jtest.TestLoggerFactory;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.groups.Tuple.tuple;
import static uk.org.lidalia.slf4jext.Level.ERROR;
import static uk.org.lidalia.slf4jext.Level.INFO;
public class Slf4jLoggerTest {
private static final Logger LOGGER = LoggerFactory.getLogger(Slf4jLoggerTest.class);
private void methodUnderTestInSomeClassInProductionCode() {
LOGGER.info("info message");
LOGGER.error("error message");
LOGGER.error("error message with exception", new RuntimeException("this part is not tested"));
}
private static final TestLogger TEST_LOGGER = TestLoggerFactory.getTestLogger(Slf4jLoggerTest.class);
#Test
public void testForMethod() throws Exception {
// when
methodUnderTestInSomeClassInProductionCode();
// then
assertThat(TEST_LOGGER.getLoggingEvents()).extracting("level", "message").contains(
tuple(INFO, "info message"),
tuple(ERROR, "error message"),
tuple(ERROR, "error message with exception")
);
}
}
This has as well the advantage to not having depend on Hamcrest matchers library.

This is an alternative using lambdas that makes the log capturing logic reusable among tests (encapsulating its implementation) and doesn't require #BeforeEach/#AfterEach (in some of the proposed solutions the appender is not detached, which can lead to memory leaks).
Code under test:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MyService {
private static final Logger LOG = LoggerFactory.getLogger(MyService.class);
public void doSomething(String someInput) {
...
LOG.info("processing request with input {}", someInput);
...
}
}
Interceptor helper:
package mypackage.util
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import org.slf4j.LoggerFactory;
import java.util.List;
public class LogInterceptor {
public static List<ILoggingEvent> interceptLogs(Class<?> klass, Runnable runnable) {
final Logger logger = (Logger) LoggerFactory.getLogger(klass);
final ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
listAppender.start();
logger.addAppender(listAppender);
try {
runnable.run();
return listAppender.list;
} finally {
logger.detachAppender(listAppender);
}
}
}
Test suite:
import static mypackage.util.LogInterceptor.interceptLogs;
public class MyServiceTest {
private MyService myService;
...
#Test
void doSomethingLogsLineWithTheGivenInput() {
List<ILoggingEvent> logs = interceptLogs(
myService.getClass(),
() -> myService.doSomething("foo")
);
assertThat(logs).isNotEmpty();
ILoggingEvent logEntry = logs.get(0);
assertThat(logEntry.getFormattedMessage()).isEqualTo("Processing request with input foo");
assertThat(logEntry.getLevel()).isEqualTo(Level.INFO);
}
}

Related

Adding Appenders to Logger interface of Log4j bridge while upgrading to Log4j2

I'm migrating a service to log4j2. A package/library in this service has custom appender implementation for one logger.
Now, due to some complexity, I'd like to use Logger interface of Logger present in Log4j bridge instead of the Log4j2 Logger. And, add appenders to this logger in the runtime. But, this doesn't work.
What works
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
public class someClass {
private static final Logger log = LogManager.getLogger("logName");
private void addAppendersToLog() {
AddAppenders.add(log);
}
}
}
import org.apache.logging.log4j.Logger;
public class AddAppenders {
public static void add(Logger log) {
//logic to add Appenders;
}
}
What doesn't work
import org.apache.log4j.Logger;
public class someClass {
// This Logger gets picked from Log4j bridge
private static final Logger log = Logger.getLogger("logName");
private void addAppendersToLog() {
AddAppenders.add(log);
}
}
}
import org.apache.log4j.Logger;
public class AddAppenders {
public static void addAppenders(Logger log) {
//logic to add Appenders;
}
}

Mocking java.util.logging.Logger while using jersey

We are using maven, jersey, Mockito in our project, with java.util.logging for Logs. While running Junit test with mocked objects, log.log() method throwing InvalidUseOfMatchersException. Below is the sample code.
import javax.inject.Inject;
import java.util.logging.Logger;
import java.util.logging.Level;
public class Sample{
#Inject
Logger log;
public String runLog(String name){
log.log(Level.INFO, "Name = "+name);
return "name";
}
}
public class SampleTest{
#InjectMocks
Sample sample;
#Mock
Logger log;
public void setup() throws Exception(){
MockitoAnnotations.initiMocks(this);
}
#Test
public void testRunLog(){
doNothing().when(log).log(any(Level.class), anyString());
String s = sample.runLog(anyString());
assertNotNull(s);
}
}
Here is the problem:
String s = sample.runLog(anyString());
You can use anyString (and all other matcher methods) only in verify and when methods.
If you don't care about exact value you pass then pass some constant or random string.

How to test a function with EntityManager in play framework 2.5

I want to test a code that is "service" level or "dao" level.
but I couldn't resolve a exception
"No EntityManager bound to this thread. Try wrapping this call in
JPAApi.withTransaction, or ensure that the HTTP context is setup on
this thread."
This is a my code for testing by junit.
and the code what make a exception is userService.createUser(); in function "createUser"
#Inject
Application application;
#Before
public void setup() {
Module testModule = new AbstractModule() {
#Override
public void configure() {
}
};
GuiceApplicationBuilder builder = new GuiceApplicationLoader()
.builder(new ApplicationLoader.Context(Environment.simple()))
.overrides(testModule);
Guice.createInjector(builder.applicationModule()).injectMembers(this);
Helpers.start(application);
}
#After
public void teardown() {
Helpers.stop(application);
}
#Test
#Transactional
public void createUser() {
running(application, () -> {
UserService userService = application.injector().instanceOf(UserService.class);
userService.createUser();
});
}
Please, Help me to resolve the exception
thx ~
As a thought, you might add another injected field:
#Inject play.db.jpa.JPAApi japApi
Then in your test you can call jpaApi.withTransaction(anotherRunnableBlock);
probably too late for JungGyu Kim, but for other who google this issue.
I found solution for version 2.6.x, but should work also for 2.5.x.
package models;
import org.junit.Before;
import org.junit.Test;
import play.Application;
import play.db.jpa.JPAApi;
import play.test.Helpers;
import play.test.WithApplication;
import java.util.stream.Stream;
import static org.junit.Assert.assertTrue;
public class MyRepositoryTest extends WithApplication {
#Override
public Application provideApplication() {
return Helpers.fakeApplication(Helpers.inMemoryDatabase());
}
JPAApi jpaApi;
MyRepository myRepository;
#Before
public void init() {
// must get instances like this, as application is created for each test
jpaApi = app.injector().instanceOf(JPAApi.class);
userRepository = app.injector().instanceOf(UserRepository.class);
// no need to setup / clear DB, WithApplication create new DB for each test
}
#Test
public void testGetAllEntries() {
// insert some data first
performNativeQuery("INSERT INTO ...");
// call tested method
final Stream<Entity> result = myRepository.getAllEntries();
// perform some assertions
assertTrue(result.count() == 1);
}
private void performNativeQuery(final String query) {
jpaApi.withTransaction(em -> em.createNativeQuery(query).executeUpdate());
}
}

Spring constructor injection of SLF4J logger - how to get injection target class?

I'm trying to use Spring to inject a SLF4J logger into a class like so:
#Component
public class Example {
private final Logger logger;
#Autowired
public Example(final Logger logger) {
this.logger = logger;
}
}
I've found the FactoryBean class, which I've implemented. But the problem is that I cannot get any information about the injection target:
public class LoggingFactoryBean implements FactoryBean<Logger> {
#Override
public Class<?> getObjectType() {
return Logger.class;
}
#Override
public boolean isSingleton() {
return false;
}
#Override
public Logger getObject() throws Exception {
return LoggerFactory.getLogger(/* how do I get a hold of the target class (Example.class) here? */);
}
}
Is FactoryBean even the right way to go? When using picocontainers factory injection, you get the Type of the target passed in. In guice it is a bit trickier. But how do you accomplish this in Spring?
Here is an alternative to your solution. You could achieve your goal with BeanFactoryPostProcessor implementation.
Let's assume you want to have a class with logging. Here it is:
package log;
import org.apache.log4j.Logger;
#Loggable
public class MyBean {
private Logger logger;
}
As you could see this class does nothing and created just to be a logger container for simplicity. The only remarkable thing here is #Loggable annotation.
Here its source code:
package log;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.TYPE)
public #interface Loggable {
}
This annotation is only a marker for further processing. And here is a most interesting part:
package log;
import org.apache.log4j.Logger;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import java.lang.reflect.Field;
public class LoggerBeanFactoryPostProcessor implements BeanFactoryPostProcessor{
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
String[] names = beanFactory.getBeanDefinitionNames();
for(String name : names){
Object bean = beanFactory.getBean(name);
if(bean.getClass().isAnnotationPresent(Loggable.class)){
try {
Field field = bean.getClass().getDeclaredField("logger");
field.setAccessible(true);
field.set(bean, Logger.getLogger(bean.getClass()));
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
It searches through all beans, and if a bean is marked as #Loggable, it initialize its private field with name logger. You could go even further and pass some parameters in #Loggable annotation. For example, it could be a name of field corresponding to logger.
I used Log4j in this example, but I guess it should work exactly the same way with slf4j.
To make your code more Spring aware use the InjectionPoint to define the loggers, i.e.:
#Bean
#Scope("prototype")
public Logger logger(InjectionPoint ip) {
return Logger.getLogger(ip.getMember().getDeclaringClass());
}
#Scope("prototype") is needed here to create 'logger' bean instance every time method is called.
I resolved it with a custom BeanFactory. If anyone comes up with a better solution, I would be happy to hear it. Anyway, here's the bean factory:
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.TypeConverter;
import org.springframework.beans.factory.config.DependencyDescriptor;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
public class CustomBeanFactory extends DefaultListableBeanFactory {
public CustomBeanFactory() {
}
public CustomBeanFactory(DefaultListableBeanFactory delegate) {
super(delegate);
}
#Override
public Object resolveDependency(DependencyDescriptor descriptor,
String beanName, Set<String> autowiredBeanNames,
TypeConverter typeConverter) throws BeansException {
//Assign Logger parameters if required
if (descriptor.isRequired()
&& Logger.class.isAssignableFrom(descriptor
.getMethodParameter().getParameterType())) {
return LoggerFactory.getLogger(descriptor.getMethodParameter()
.getDeclaringClass());
} else {
return super.resolveDependency(descriptor, beanName,
autowiredBeanNames, typeConverter);
}
}
}
Example usage with an XML config:
CustomBeanFactory customBeanFactory = new CustomBeanFactory();
GenericApplicationContext ctx = new GenericApplicationContext(customBeanFactory);
XmlBeanDefinitionReader xmlReader = new XmlBeanDefinitionReader(ctx);
xmlReader.loadBeanDefinitions(new ClassPathResource("beans.xml"));
ctx.refresh();
EDIT:
Below you can find Arend v. Reinersdorffs improved version (see the comments for an explanation).
import java.lang.reflect.Field;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.TypeConverter;
import org.springframework.beans.factory.config.DependencyDescriptor;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.core.MethodParameter;
public class CustomBeanFactory extends DefaultListableBeanFactory {
public CustomBeanFactory() {
}
public CustomBeanFactory(DefaultListableBeanFactory delegate) {
super(delegate);
}
#Override
public Object resolveDependency(DependencyDescriptor descriptor,
String beanName, Set<String> autowiredBeanNames,
TypeConverter typeConverter) throws BeansException {
//Assign Logger parameters if required
if (Logger.class == descriptor.getDependencyType()) {
return LoggerFactory.getLogger(getDeclaringClass(descriptor));
} else {
return super.resolveDependency(descriptor, beanName,
autowiredBeanNames, typeConverter);
}
}
private Class<?> getDeclaringClass(DependencyDescriptor descriptor) {
MethodParameter methodParameter = descriptor.getMethodParameter();
if (methodParameter != null) {
return methodParameter.getDeclaringClass();
}
Field field = descriptor.getField();
if (field != null) {
return field.getDeclaringClass();
}
throw new AssertionError("Injection must be into a method parameter or field.");
}
}
Try something like:
#Component
public class Example {
#Autowired
#Qualifier("exampleLogger")
private final Logger logger;
}
And:
<bean id="exampleLogger" class="org.slf4j.LoggerFactory" factory-method="getLogger">
<constructor-arg type="java.lang.Class" value="package.Example"/>
</bean>
Since Spring 4.3.0 you can use InjectionPoint or DependencyDescriptor as parameters for bean producing methods:
#Component
public class LoggingFactoryBean {
#Bean
public Logger logger(InjectionPoint injectionPoint) {
Class<?> targetClass = injectionPoint.getMember().getDeclaringClass();
return LoggerFactory.getLogger(targetClass);
}
}
Yeah, you are going in the wrong direction. If I were you I would inject the LoggerFactory. If you want to hide that it is slf4j then I'd define a LoggerFactory interface and inject a class which delegates through to slf4j Logger.
public interface LoggerFactory {
public Logger getLogger(Class<?> clazz);
}
...
import org.slf4j.LoggerFactory;
public class Slf4jLoggerFactory implements LoggerFactory {
public Logger getLogger(Class<?> clazz) {
return org.slf4j.LoggerFactory.getLogger(clazz);
}
}
However, before you go there, this is approximately what org.apache.commons.logging is doing right? http://commons.apache.org/logging/
You use Log's instead of Loggers:
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class CLASS {
private Log log = LogFactory.getLog(CLASS.class);
...
Apache then looks through the classpath to see if you have log4j or others and delegates to the "best" one that it finds. Slf4j replaces log4j in the classpath so if you have it loaded (and apache log4j excluded) commons logging will delegate to it instead.
Why are you creating a new logger for each instance? The typical pattern is to have one logger per class (as a private static member).
If you really do want to do it that way: Maybe you can write a logger factory class, and inject that? Something like:
#Singleton
public class LogFactory {
public Logger getLogger(Object o) {
return LoggerFactory.getLogger(o.getClass());
}
}
I am trying to get this feature into official SLF4J API. Please support/vote/contribute: https://issues.jboss.org/browse/JBLOGGING-62
(this feature is already implemented by JBoss Logging + Seam Solder, see http://docs.jboss.org/seam/3/latest/reference/en-US/html/solder-logging.html)
11.4. Native logger API
You can also inject a "plain old" Logger (from the JBoss Logging API):
import javax.inject.Inject;
import org.jboss.logging.Logger;
public class LogService {
#Inject
private Logger log;
public void logMessage() {
log.info("Hey sysadmins!");
}
}
Log messages created from this Logger will have a category (logger name) equal to the fully-qualified class name of the bean implementation class. You can specify a category explicitly using an annotation.
#Inject #Category("billing")
private Logger log;
You can also specify a category using a reference to a type:
#Inject #TypedCategory(BillingService.class)
private Logger log;
Sorry for not providing a relevant answer.

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