How to intercept SLF4J with logback logging via a JUnit test?
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
Sometimes a unit test should verify that code logs the right message, not just that it returns the right value. That happens when logging is part of the behavior contract, such as warning on invalid input, recording a correlation ID, or suppressing noisy stack traces. With SLF4J backed by Logback, the cleanest way to test logging is usually to attach a temporary in-memory appender and assert on the captured events.
Why You Test the Backend, Not SLF4J Alone
SLF4J is only a logging facade. It defines the API your code calls, but it does not store log events itself. The actual capture happens in the backend, which in this case is Logback.
That is why a JUnit test usually works with Logback classes such as:
- '
ch.qos.logback.classic.Logger' - '
ListAppender' - '
ILoggingEvent'
The test does not replace SLF4J. It temporarily observes the concrete Logback logger used by the class under test.
A Small Example Class
Suppose a service logs a warning when someone tries to reserve an invalid quantity:
A useful test can assert that invalid input produces a WARN event with the expected rendered message.
Capture Logs with ListAppender
Here is a JUnit 5 test that intercepts the logging output:
This is usually enough for application-level logging tests.
What to Assert On
The captured event gives you several useful angles:
- '
getLevel()forINFO,WARN, orERROR' - '
getFormattedMessage()for the final rendered message' - '
getThrowableProxy()for attached exceptions' - '
getMDCPropertyMap()for request IDs or other contextual data'
For placeholder-based logging, prefer getFormattedMessage() over getMessage(). The raw message still contains the template with placeholder markers, while the formatted message shows what an operator would actually read in logs.
Testing MDC and Structured Context
If your application depends on MDC values, assert on those too:
This is useful when you need confidence that log enrichment survives refactors.
Keep the Logger State Isolated
Loggers are shared state inside the JVM, so cleanup matters. Attach the appender in @BeforeEach, detach it in @AfterEach, and restore any level changes you make during the test.
If you raise or lower the level, save the old one first:
Without cleanup, one logging test can leak configuration into the next one and create confusing failures.
When This Style of Test Is Worth It
Do not test every log line in the codebase. That becomes brittle quickly. Logging assertions are most useful when the log output matters to system behavior or operational support, for example:
- security audit events
- warnings for rejected input
- messages consumed by alerting rules
- correlation or trace identifiers required in production
That keeps the tests meaningful instead of turning them into low-value snapshots of implementation detail.
Common Pitfalls
One common mistake is attaching the appender to the wrong logger name. If the production class logs through InventoryService.class, but the test captures some other logger, the list stays empty and the test gives a misleading failure.
Another mistake is forgetting appender.start(). A ListAppender that is never started will not capture anything.
Developers also often assert on getMessage() when the code uses placeholder arguments. That checks the template instead of the rendered message.
Finally, always detach the appender after the test. Logging infrastructure is shared, and leaked appenders can make later tests capture unrelated events.
Summary
- With SLF4J and Logback, test logging by attaching a temporary
ListAppender. - Capture the exact logger used by the class under test.
- Prefer
getFormattedMessage()when asserting placeholder-based log statements. - Clean up appenders and any temporary level changes after each test.
- Log assertions are most valuable when logging is part of the observable contract, not just incidental noise.

