Usage
Making Assertions
TestLoggerFactory is the underlying implementation to which SLF4J's LoggerFactory delegates when SLF4J Test is the bound SLF4J implementation. The getLogger method on TestLoggerFactory returns the same Logger instance as that on LoggerFactory, but with the additional type information to allow retrieval of the LoggingEvents that have occurred against that Logger.
SLF4J Test provides a comprehensive set of static factory methods on LoggingEvent to facilitate easy construction of expected events for comparison against those that were actually received, and LoggingEvent has appropriate equals, hashCode and toString implementations for this purpose. See the front page for an example of using the info static factory method to create an expected LoggingEvent.
Custom Predicates
Users can provide custom predicates so that they can assert on LoggingEvent
s in specific ways in addition to the regular supported ways.
Example:
assertThat(logger)
.hasLogged(event -> event.getFormattedMessage().startsWith("First section of long log message"));
MDC Comparison Strategy
The default behaviour requires the MDC contents to match exactly. But since the full MDC context isn't always relevant to the test, the MDC comparison strategy can be specified by users.
Example:
assertThat(logger)
.usingMdcComparator(MdcComparator.IGNORING)
.hasLogged(warn("Some log message"));
Setting the Log Level on a Logger
SLF4J Test only stores events for levels which are marked as enabled on the Logger. By default, all levels are enabled; however, this can be programmatically changed on a per logger basis using the following functions:
Logger.setEnabledLevels(Level... levels)
Logger.setEnabledLevelsForAllThreads(Level... levels)
It's important to note that SLF4J does not imply any relationship between the levels - it is perfectly possible for a logger to be enabled for INFO but disabled for ERROR as far as SLF4J is concerned, and hence as far as SLF4J Test is concerned, notwithstanding the fact that implementations such as Logback and Log4J do not permit this.
Globally disabling a Log Level
Storing log events at all levels can slow down test executions. If needed a global setting can be used to avoid capturing events at a given level or below respecting the conventional level hierarchy (if the capture level is set to INFO, DEBUG and TRACE events won't be captured). This can be set in any of the following ways:
Programmatically
TestLoggerFactory.getInstance().setCaptureLevel(Level.INFO);
Via a System Property
Run the JVM with the following:
-Dslf4jtest.capture.level=INFO
Via a properties file
Place a file called slf4jtest.properties on the classpath with the following line in it:
capture.level=INFO
Resetting Stored State
In order to have robust tests the in memory state of SLF4J Test must be in a known state for each test run, which in turn implies that a test should clean up after itself. The simplest way to do so is via a call to
TestLoggerFactory.clear()
in a tear down method of some kind.
More nuanced state resetting can be done on a per logger basis:
TestLogger.clear()
or more aggressive clears will reset state across all threads:
TestLoggerFactory.clearAll()
TestLogger.clearAll()
JUnit 4
If you are using JUnit 4 then SLF4J Test provides a @Rule
that will do this
for you if you provide the following line in your test class:
@Rule public TestRule resetLoggingEvents = new TestLoggerFactoryResetRule();
JUnit 5
SLF4J Test provides JUnit Platform extension which can be registered in any supported way.
Declarative Extension Registration
TestLoggerFactoryExtension
can be registered declaratively via @ExtendWith
annotation:
import org.junit.jupiter.api.extension.ExtendWith;
import com.github.valfirst.slf4jtest.TestLoggerFactoryExtension;
@ExtendWith(TestLoggerFactoryExtension.class)
class BasicJUnit5Test {
...
}
It is possible to configure stage which cleanup should be performed at
using @TestLoggerFactorySettings
annotation:
import org.junit.jupiter.api.extension.ExtendWith;
import com.github.valfirst.slf4jtest.CleanupStage;
import com.github.valfirst.slf4jtest.TestLoggerFactoryExtension;
import com.github.valfirst.slf4jtest.TestLoggerFactorySettings;
@ExtendWith(TestLoggerFactoryExtension.class)
@TestLoggerFactorySettings(cleanupStage = CleanupStage.BEFORE_EACH)
class BasicJUnit5Test {
...
}
Programmatic Extension Registration
TestLoggerFactoryExtension
can be registered programmatically by annotating field in test classes
with @RegisterExtension
:
import org.junit.jupiter.api.extension.RegisterExtension;
import com.github.valfirst.slf4jtest.TestLoggerFactoryExtension;
class BasicJUnit5Test {
@RegisterExtension
static TestLoggerFactoryExtension extension = new TestLoggerFactoryExtension();
...
}
Also, it is possible to configure stage which clean up should happen at:
import org.junit.jupiter.api.extension.RegisterExtension;
import com.github.valfirst.slf4jtest.CleanupStage;
import com.github.valfirst.slf4jtest.TestLoggerFactoryExtension;
class BasicJUnit5Test {
@RegisterExtension
static TestLoggerFactoryExtension extension = new TestLoggerFactoryExtension(CleanupStage.BEFORE_EACH);
...
}
Automatic Extension Registration
SLF4J Test supports automatic extension registration via ServiceLoader mechanism. This feature is considered advanced in JUnit Platform and it requires explicit enabling.
Parallel Testing
SLF4J Test is designed to facilitate tests run in parallel. Because SLF4J Loggers are commonly shared across threads, the SLF4J Test implementation maintains its state in ThreadLocals. The following functions:
TestLogger.getLoggingEvents()
TestLogger.clear()
TestLogger.getEnabledLevels()
TestLogger.setEnabledLevels(Level... levels)
TestLoggerFactory.getLoggingEvents()
TestLoggerFactory.clear()
all only affect state stored in a ThreadLocal, and thus tests which use them can safely be parallelised without any danger of concurrent test runs affecting each other.
Testing Multiple Threads Logging
At times, however, it may be desirable to assert about the log messages produced by concurrent code. To facilitate this SLF4J Test also maintains a record of logging events from all threads. This state can be accessed and reset using the following functions:
TestLogger.getAllLoggingEvents()
TestLogger.clearAll()
TestLogger.setEnabledLevelsForAllThreads(Level... levels)
TestLoggerFactory.getAllLoggingEvents()
TestLoggerFactory.clearAll()
Printing log statements to System out and err
It can still be useful to print log messages to System out/err as appropriate. SLF4J Test will print messages using a standard (non-configurable) format based on the value of the TestLoggerFactory's printLevel property. For convenience this does respect the conventional level hierarchy where if the print level is INFO logging events at levels WARN and ERROR will also be printed. A level that is disabled globally will not be printed.
This can be set in any of the following ways:
Programmatically
TestLoggerFactory.getInstance().setPrintLevel(Level.INFO);
Via a System Property
Run the JVM with the following:
-Dslf4jtest.print.level=INFO
Via a properties file
Place a file called slf4jtest.properties on the classpath with the following line in it:
print.level=INFO
Testing Code Using java.util.logging
Although this module is named SLF4J Test, it can be used to test code using
other test API's as well. In most cases, it is just a matter of placing the
proper bridge on the test classpath, see e.g.
Bridging legacy APIs.
But in case of java.util.logging, there is a little more work than adding
jul-to-slf4j
to the classpath. Normally,
an application will create a logging.properties
file containing
handlers = org.slf4j.bridge.SLF4JBridgeHandler
and set the system property java.util.logging.config.file
on the
command line to the full path of the file. But this is not
practical for running unit tests. Instead, the configuration is done
programmatically in the test code.
SLF4J Test has code to make this easier. Calling
JulConfig.setup()
will do this for you. You should call this method before running any
test that requires java.util.logging to be configured. Calling this method more
than once has no effect, so you can safely call it before tests in all test
suites with this requirement, e.g. in a @BeforeClass
method if using Junit 4.
If you use Junit 5, it is even easier. There is a Junit 5 extension
JulConfigExtension
doing this declaratively. For example
import com.github.valfirst.slf4jtest.JulConfigExtension;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(JulConfigExtension.class)
class JulLoggingTests {
@Test
void testJulLogging() {
...
}
...
}
Customizing the MDC
The MDC (original meaning Mapped Diagnostic Context) is a place that can be used to store additional information as key/value pairs. The information is stored by thread. It can be set by the client code and used by the logging backend. The specification of the MDC in SLF4J leaves much detail up to the logging backend.
SLF4J Test lets the user customize the behavior of the MDC to more closely match that of the backend expected to be used at run-time.
The configuration can be changed in the property file slf4jtest.properties
,
using system properties specified as command line arguments to the JVM,
or programmatically. The property file and the system properties are read
at start-up time, with system properties overriding the property file.
Note that changing the values programmatically affects all threads.
Completely Disable the MDC
The backend does not even have to implement the MDC at all. If the log4j2 adapter or logback is used, there is full support for the MDC. Using the java.util.logging adapter, there is an MDC, but it cannot be used by log appenders. In the slf4j-simple, the MDC implementation is a no-op.
It is possible to disable the MDC completely by specifying
Programmatically
TestMDCAdapter.getInstance().setEnable(false);
Via a System Property
-Dslf4jtest.mdc.enable=false
In the slf4j.properties File
mdc.enable=false
The default value is true
. If disabled, the get
and getCopyOfContextMap
methods return null, and all other methods are no-ops.
Inheritance from the Parent Thread
It is unspecified whether a child thread inherits the MDC from its parent. In log4j2 and logback, the MDC is not inherited by default, but this behavoir can be changed by the configuration. Using the java.util.logging adapter, inheritance is always on.
Programmatically
TestMDCAdapter.getInstance().setInherit(true);
Via a System Property
-Dslf4jtest.mdc.inherit=true
In the slf4j.properties File
mdc.inherit=true
The default value is false
.
Allow null values
In the description of the
put
method, it says “The val
parameter can be null only if the underlying
implementation supports it.”
To emulate a backend that does not support null values, this can be configured.
Programmatically
TestMDCAdapter.getInstance().setAllowNullValues(false);
Via a System Property
-Dslf4jtest.mdc.allow.null.values=false
In the slf4j.properties File
mdc.allow.null.values=false
The default value is true
.
Note that if the MDC functionality has been completely disabled, null
values are accepted and ignored, regardless of this setting.
Null Returned for Empty Map
The description of the
getCopyOfContextMap
method says “Returned value may be null.”, but it is not specified under
which circumstances it may be null. Obviously, a backend that does not
support an MDC will return null. The implementation in the
java.util.logging adapter returns null, if put
has not been called.
Other implementations may return an empty map in that case.
To change the behavior:
Programmatically
TestMDCAdapter.getInstance().setReturnNullCopyWhenMdcNotSet(true);
Via a System Property
-Dslf4jtest.mdc.return.null.copy.when.mdc.not.set=true
In the slf4j.properties File
mdc.return.null.copy.when.mdc.not.set=true
The default value is false
, in which case an empty map is returned
when the MDC is not set.
Note that if the MDC functionality has been completely disabled,
null
is returned, regardless of this setting.
Restoring Options
To restore these options to the values defined by the initial configuration, use
TestMDCAdapter.getInstance().restoreOptions();
Using the Fluent API
From SLF4J version 2.0, there is a new fluent logging API. It allows writing code like
logger.atInfo().setMessage("With an argument {}")
.addArgument(() -> myObj.calculateValue())
.addMarker("ForYourEyesOnly").log();
The atInfo()
method returns a LoggingEventBuilder
which is used to
build the event and finally log it.
If logging at INFO level is not enabled, a NOOP LoggingEventBuilder
is
returned by atInfo
. This means the lambda on the second line will not be
executed, thus saving the overhead if the message is not logged.
Note that it is possible to add multiple markers to an event, which is not possible with the classic API.
SLF4J Test uses the TestLoggingEventBuilder
, which is a modified version of
the DefaultLoggingEventBuilder
of SLF4J.
Creating an SLF4J Test LoggingEvent
to compare against can be done
in the classic way like
LoggingEvent.info(
MarkerFactory.getMarker("ForYourEyesOnly"),
"With an argument {}",
myObj.calculateValue());
Alternatively, you can create the LoggingEvent
from an existing SLF4J
logging event, like
LoggingEvent.fromSlf4jEvent(
new TestLoggingEventBuilder(null, Level.INFO)
.setMessage("With an argument {}")
.addArgument(() -> myObj.calculateValue())
.addMarker(MarkerFactory.getMarker("ForYourEyesOnly"))
.toLoggingEvent());
This approach is necessary if you use the features available in the fluent API only. This includes multiple markers and key/value pairs.
The TestLoggingEventBuilder
adds the toLoggingEvent
method
to access the created event. Please note that the returned value is an
org.slf4j.event.LoggingEvent
,
which is different from com.github.valfirst.slf4jtest.LoggingEvent
.