@TestLogger

Loggers are often defined as private static final fields. That makes them difficult to mock.

Using @TestLogger, @TestLogger.ForClass or @TestLogger.Root you can inject a so-called logger context that allows you to configure a logger:

  • Setting the level
  • Add, remove or replace appenders / handlers
  • Enable or disable inheriting appenders / handlers from the parent logger
  • Capturing logging events / records

When the logger context goes out of scope (when injected as a field or method parameter, this is when the test ends), all original settings are restored.

Supported logging frameworks

The following logging framework implementations are supported:

java.util.logging

Class JdkLoggerContext can be used for the logger context when java.util.logging is used as logging implementation. This is true not only when using java.util.logging.Logger directly, but also when java.util.logging is used through dependencies like org.slf4j:slf4j-jdk14 or org.apache.logging.log4j:log4j-jul.

Log4j core

Class Log4jLoggerContext can be used for the logger context when Log4j 2.x is used as logging implementation. This means that org.apache.logging.log4j:log4j-core must be used. When using org.apache.logging.log4j:log4j-api with a different implementation (e.g. org.apache.logging.log4j:log4j-jul), class Log4jLoggerContext can not be used.

Appender mocking

Unlike appenders / handlers for other logging frameworks, Log4j appenders cannot be easily provided as mocks for the following reasons:

  • Appenders must have a name
  • Appenders should be started
  • The event passed to appenders may be mutable and shared; capturing them may result in unexpected results

Class Log4jNullAppender has been created to overcome these issues. Log4jNullAppender.create creates a named, started appender. Its append method has been implemented to turn the event into an immutable event, which is then passed to the ignore method. This method is safe to be spied upon:

Log4jNullAppender appender = spy(Log4jNullAppender.create("mock"));

// perform calls that will trigger the appender

ArgumentCaptor<LogEvent> eventCaptor = ArgumentCaptor.forClass(LogEvent.class);
verify(appender, atLeastOnce()).ignore(eventCaptor.capture());
List<LogEvent> events = eventCaptor.getAllValues();
// perform assertions on events

Logback

Class LogbackLoggerContext can be used for the logger context when Logback is used as logging implementation. Since logback is a native SLF4J implementation, SLF4J should not have any other bindings.

Reload4j

Class Reload4jLoggerContext can be used for the logger context when reload4j is used as logging implementation. This is true not only when using reload4 directly, but also when reload4j is used through dependencies like org.slf4j:slf4j-reload4j.

Since reload4j is a fork of Log4j 1.2.17, Reload4jLoggerContext should also be usable for when using Log4j 1.2.17 (possibly through dependency org.slf4j:slf4j-log4j12).

API based frameworks

For API based frameworks like SLF4J and Log4j, the logger context class to used depends on the binding. For instance, use class JdkLoggerContext if the binding is org.slf4j:slf4j-jdk14 or org.apache.logging.log4j:log4j-jul, etc.

Examples

Using method injection:

@Test
void testLogging(@TestLogger.ForClass(MyClass.class) Reload4jLoggerContext loggerContext) {
    Appender appender = mock(Appender.class);
    loggerContext
            .setLevel(Level.INFO)
            .useParentAppenders(false)
            .setAppender(appender);

    // perform calls that trigger the logger

    ArgumentCaptor<LoggingEvent> eventCaptor = ArgumentCaptor.forClass(LoggingEvent.class);
    verify(appender, atLeastOnce()).doAppend(eventCaptor.capture());
    List<LoggingEvent> events = eventCaptor.getAllValues();
    // perform assertions on events
}

Using field injection:

@TestLogger.ForClass(MyClass.class)
private Reload4jLoggerContext loggerContext;

private Appender appender;

@BeforeEach
void configureLogger() {
    appender = mock(Appender.class);
    loggerContext
            .setLevel(Level.INFO)
            .useParentAppenders(false)
            .setAppender(appender);
}

@Test
void testLogging() {
    // perform calls that trigger the logger

    ArgumentCaptor<LoggingEvent> eventCaptor = ArgumentCaptor.forClass(LoggingEvent.class);
    verify(appender, atLeastOnce()).doAppend(eventCaptor.capture());
    List<LoggingEvent> events = eventCaptor.getAllValues();
    // perform assertions on events
}

The above can also be achieved as follows:

@Test
void testLogging(@TestLogger.ForClass(MyClass.class) Reload4jLoggerContext loggerContext) {
    LogCaptor<LoggingEvent> logCaptor = loggerContext
            .setLevel(Level.INFO)
            .useParentAppenders(false)
            .capture();

    // perform calls that trigger the logger

    List<LoggingEvent> events = logCaptor.logged();
    // perform assertions on events
}