Testing Your SLF4J Logs

Logging is a very important part of your application. You can be the best developer and never write a bug, but a service you depend on may go down, or the framework you're using may have an issue, so you need to be able to diagnose it without attaching a debugger to your production server. But if you're not testing that your logging works, you won't be sure until one of these disaster scenarios occur whether it's actually hooked in correctly, as well as not being able to catch if someone accidentally removes a logging line in a refactor.

A very popular logging library in the Java ecosystem is SLF4J, and we'll look at how we can test we've set things up correctly.

Update 2020-05-06: I would recommend seeing this note about sfl4j-test, which I now recommend over my below solution.

The repository for this article can be found at jamietanna/slf4j-testing, and the examples are based on davidxxx's response on Stack Overflow.

Let's say that we have this example class that does some processing of data, as well as logging:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ClassThatLogs {

  private static final Logger LOGGER = LoggerFactory.getLogger(ClassThatLogs.class);

  public void doSomething(boolean doSomethingElse) {
    LOGGER.debug("The boolean passed in has value {}", logErrors);
    // do some stuff
    LOGGER.info("this is logging something else");
    // do some other stuff
  }
}

To test that these logs are send correctly, we're able to hook into the underlying Logback code and get a ListAppender object, which contains all the log events for a given Logger. Note that it's best to this into separate utility class so we can re-use the code around the project, instead of having these lines duplicated across test classes.

import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import org.slf4j.LoggerFactory;

public class LoggerTestUtil {
  public static ListAppender<ILoggingEvent> getListAppenderForClass(Class clazz) {
    Logger logger = (Logger) LoggerFactory.getLogger(clazz);

    ListAppender<ILoggingEvent> loggingEventListAppender = new ListAppender<>();
    loggingEventListAppender.start();

    logger.addAppender(loggingEventListAppender);

    return loggingEventListAppender;
  }
}

Now we have a straightforward setup, and can take advantage of AssertJ to make it easier to assert that the logs are present at the right level:

import static org.assertj.core.api.Java6Assertions.assertThat;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import org.assertj.core.groups.Tuple;
import org.junit.Before;
import org.junit.Test;

public class ClassThatLogsTest {

  private ListAppender<ILoggingEvent> loggingEventListAppender;
  private ClassThatLogs sut;

  @Before
  public void setup() {
    loggingEventListAppender = LoggerTestUtil.getListAppenderForClass(ClassThatLogs.class);

    sut = new ClassThatLogs();
  }

  // other tests

  @Test
  public void methodLogsInfoRegardless() {
    // given

    // when
    sut.doSomething(false);

    // then
    assertThat(loggingEventListAppender.list)
        .extracting(ILoggingEvent::getMessage, ILoggingEvent::getLevel)
        .contains(Tuple.tuple("this is logging something else", Level.INFO));
  }

  @Test
  public void methodLogsFormatStringsInDebugMode() {
    // given

    // when
    sut.doSomething(false);

    // then
    assertThat(loggingEventListAppender.list)
        .extracting(ILoggingEvent::getMessage, ILoggingEvent::getFormattedMessage,
            ILoggingEvent::getLevel)
        .contains(Tuple
            .tuple("The boolean passed in has value {}", "The boolean passed in has value false",
                Level.DEBUG));
  }
}

Notice that when we are using a format string we have two checks on the log message - getMessage returns the raw message (including format placeholders), but getFormattedMessage returns the expanded log message.

As Jack Gough pointed out, there is also the library slf4j-test. I had originally not put this as the source repository does not seem to have a license - it appears the license is somewhere on the website, because I found a Pull Request raised to add that the license to the repo. The project seems dormant as there have been no updates in over 4 years, and there are PRs open since 2016, so use at your own risk! Update 2020-05-06: I am using this in production, so you may want to, as well!

Written by Jamie Tanna's profile image Jamie Tanna on , and last updated on .

Content for this article is shared under the terms of the Creative Commons Attribution Non Commercial Share Alike 4.0 International, and code is shared under the Apache License 2.0.

#blogumentation #testing #java.

This post was filed under articles.

Has this content helped you? Please consider supporting me so I can continue to create content like this!

Related Posts

Other posts you may be interested in:

Interactions with this post

Interactions with this post

Below you can find the interactions that this page has had using WebMention.

Have you written a response to this post? Let me know the URL:

Do you not have a website set up with WebMention capabilities? You can use Comment Parade.