1. Introduction

In this tutorial, we’ll look at how we can cover generated logs in JUnit testing.

We’ll use the slf4j-api and the logback implementation and create a custom appender that we can use for log assertion.

2. Maven Dependencies

Before we begin, let’s add the logback dependency. As it natively implements the slf4j-api, it is automatically downloaded and injected into the project by Maven transitivity:

<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>. 
    <version>1.2.6</version>
</dependency>

AssertJ offers very useful functions when testing, so let’s add its dependency to the project as well:

<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <version>3.15.0</version>
    <scope>test</scope>
</dependency>

3. A Basic Business Function

Now, let’s create an object that will generate logs we can base our tests on.

Our BusinessWorker object will only expose one method. This method will generate a log with the same content for each log level. Although this method isn’t that useful in the real world, it’ll serve well for our testing purposes:

public class BusinessWorker {
    private static Logger LOGGER = LoggerFactory.getLogger(BusinessWorker.class);

    public void generateLogs(String msg) {
        LOGGER.trace(msg);
        LOGGER.debug(msg);
        LOGGER.info(msg);
        LOGGER.warn(msg);
        LOGGER.error(msg);
    }
}

4. Testing the Logs

We want to generate logs, so let’s create a logback.xml file in the src/test/resources folder. Let’s keep it as simple as possible and redirect all logs to a CONSOLE appender:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <layout class="ch.qos.logback.classic.PatternLayout">
            <Pattern>
                %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n
            </Pattern>
        </layout>
    </appender>

    <root level="error">
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>

4.1. MemoryAppender

Now, let’s create a custom appender that keeps logs in memory. We’ll extend the ListAppender that logback offers, and we’ll enrich it with a few useful methods:

public class MemoryAppender extends ListAppender<ILoggingEvent> {
    public void reset() {
        this.list.clear();
    }

    public boolean contains(String string, Level level) {
        return this.list.stream()
          .anyMatch(event -> event.toString().contains(string)
            && event.getLevel().equals(level));
    }

    public int countEventsForLogger(String loggerName) {
        return (int) this.list.stream()
          .filter(event -> event.getLoggerName().contains(loggerName))
          .count();
    }

    public List<ILoggingEvent> search(String string) {
        return this.list.stream()
          .filter(event -> event.toString().contains(string))
          .collect(Collectors.toList());
    }

    public List<ILoggingEvent> search(String string, Level level) {
        return this.list.stream()
          .filter(event -> event.toString().contains(string)
            && event.getLevel().equals(level))
          .collect(Collectors.toList());
    }

    public int getSize() {
        return this.list.size();
    }

    public List<ILoggingEvent> getLoggedEvents() {
        return Collections.unmodifiableList(this.list);
    }
}

The MemoryAppender class handles a List that is automatically populated by the logging system.

It exposes a variety of methods in order to cover a wide range of test purposes:

  • reset() – clears the list
  • contains(msg, level) – returns true only if the list contains an ILoggingEvent matching the specified content and severity level
  • countEventForLoggers(loggerName) – returns the number of ILoggingEvent generated by named logger
  • search(msg) – returns a List of ILoggingEvent matching the specific content
  • search(msg, level) – returns a List of ILoggingEvent matching the specified content and severity level
  • getSize() – returns the number of ILoggingEvents
  • getLoggedEvents() – returns an unmodifiable view of the ILoggingEvent elements

4.2. Unit Test

Next, let’s create a JUnit test for our business worker.

We’ll declare our MemoryAppender as a field and programmatically inject it into the log system. Then, we’ll start the appender.

For our tests, we’ll set the level to DEBUG:

@Before
public void setup() {
    Logger logger = (Logger) LoggerFactory.getLogger(LOGGER_NAME);
    memoryAppender = new MemoryAppender();
    memoryAppender.setContext((LoggerContext) LoggerFactory.getILoggerFactory());
    logger.setLevel(Level.DEBUG);
    logger.addAppender(memoryAppender);
    memoryAppender.start();
}

Now we can create a simple test where we instantiate our BusinessWorker class and call the generateLogs method. We can then make assertions on the logs that it generates:

@Test
public void test() {
    BusinessWorker worker = new BusinessWorker();
    worker.generateLogs(MSG);
        
    assertThat(memoryAppender.countEventsForLogger(LOGGER_NAME)).isEqualTo(4);
    assertThat(memoryAppender.search(MSG, Level.INFO).size()).isEqualTo(1);
    assertThat(memoryAppender.contains(MSG, Level.TRACE)).isFalse();
}

This test uses three features of the MemoryAppender:

  • Four logs have been generated — one entry per severity should be present, with the trace level filtered
  • Only one log entry with the content message with the level severity of INFO
  • No log entry is present with content message and severity TRACE

If we plan to use the same instance of this class inside the same test class when generating a lot of logs, the memory usage will creep up. We can invoke the MemoryAppender.clear()* method before each test to free memory and avoid *OutOfMemoryException.

In this example, we’ve reduced the scope of the retained logs to the LOGGER_NAME package, which we defined as “com.baeldung.junit.log“. We could potentially retain all logs with LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME), but we should avoid this whenever possible as it can consume a lot of memory.

5. Conclusion

With this tutorial, we’ve demonstrated how to cover log generation in our unit tests.

As always, the code can be found over on GitHub.