Originally published on 2010-03-09; fully revised and updated.
If your JUnit 4 tests still rely on ad-hoc System.out.println() lines, this pattern gives you a cleaner path: use SLF4J for consistent logging, add parameterized messages for safer performance, and centralize test lifecycle logs with JUnit rules. This is useful for developers who want clearer local debugging and less noisy CI test output.

Why test logging quality matters in JUnit 4
Quick print statements are easy at first, but they become hard to maintain across many tests. Structured logging improves readability, supports log levels, and lets teams route logs through their existing logging backend without changing test code.
- Better signal: consistent format and severity levels.
- Less duplication: avoid repeating method names in every test body.
- Backend flexibility: SLF4J lets projects choose Logback, Log4j, or another binding later.
Minimal dependency setup for the examples
If your project does not already include JUnit and SLF4J, a Maven setup typically includes JUnit, slf4j-api, and one SLF4J binding implementation.
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>YOUR_JUNIT4_VERSION</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>YOUR_SLF4J_VERSION</version>
</dependency>
<!-- Choose one SLF4J binding implementation for runtime/test runtime -->
</dependencies>Code language: HTML, XML (xml)
Logging approaches compared
| Approach | Example | Strengths | Trade-offs |
|---|---|---|---|
| System.out.println() | System.out.println("testA being run..."); | Fastest to write | No levels, no backend control, easy to overuse |
| Direct SLF4J in each test | logger.info("testA being run..."); | Structured logging, backend-agnostic | Repeated strings and repeated lines in each method |
| SLF4J + TestName rule | logger.info("{} being run...", name.getMethodName()); | No hard-coded method names | Still repetitive if repeated in every test |
| SLF4J + watcher rule | starting/succeeded/failed hooks | DRY lifecycle logging across all tests | Rule API details depend on JUnit 4 style used in the codebase |
Step-by-step refactor from println to reusable lifecycle logging
1) Baseline: println logging
import org.junit.Test;
public class LoggingTest {
@Test
public void testA() {
System.out.println("testA being run...");
}
@Test
public void testB() {
System.out.println("testB being run...");
}
}Code language: JavaScript (javascript)
2) Switch to SLF4J
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LoggingTest {
private static final Logger logger = LoggerFactory.getLogger(LoggingTest.class);
@Test
public void testA() {
logger.info("testA being run...");
}
@Test
public void testB() {
logger.info("testB being run...");
}
}Code language: JavaScript (javascript)
3) Remove hard-coded names with TestName and parameterized messages
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LoggingTest {
@Rule
public TestName name = new TestName();
private static final Logger logger = LoggerFactory.getLogger(LoggingTest.class);
@Test
public void testA() {
logger.info("{} being run...", name.getMethodName());
}
@Test
public void testB() {
logger.info("{} being run...", name.getMethodName());
}
}Code language: JavaScript (javascript)
4) Apply DRY with a watcher rule
The original pattern used TestWatchman. Many JUnit 4 codebases use TestWatcher for similar lifecycle hooks. Use the style that matches your project’s JUnit 4 API.
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.MethodRule;
import org.junit.rules.TestWatchman;
import org.junit.runners.model.FrameworkMethod;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LoggingTest {
private static final Logger logger = LoggerFactory.getLogger(LoggingTest.class);
@Rule
public MethodRule watchman = new TestWatchman() {
@Override
public void starting(FrameworkMethod method) {
logger.info("{} being run...", method.getName());
}
};
@Test
public void testA() {}
@Test
public void testB() {}
}Code language: JavaScript (javascript)
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LoggingTest {
private static final Logger logger = LoggerFactory.getLogger(LoggingTest.class);
@Rule
public TestWatcher watcher = new TestWatcher() {
@Override
protected void starting(Description description) {
logger.info("{} being run...", description.getMethodName());
}
@Override
protected void succeeded(Description description) {
logger.info("{} passed", description.getMethodName());
}
@Override
protected void failed(Throwable e, Description description) {
logger.error("{} failed", description.getMethodName(), e);
}
};
@Test
public void testA() {}
@Test
public void testB() {}
}Code language: JavaScript (javascript)
Watcher-based logging workflow
flowchart TD
A[JUnit starts test class] --> B[Rule starting hook logs method]
B --> C[Test method executes]
C --> D{Test outcome}
D -- passed --> E[Rule succeeded hook logs pass]
D -- failed --> F[Rule failed hook logs error]
E --> G[Rule finished hook runs]
F --> G
G --> H[JUnit reports result]
Performance note on parameterized logging
The original article cites SLF4J guidance that parameterized logging can be much more efficient than string concatenation when a log level is disabled (quoted there as at least a 30x factor in that case). Treat that number as context from older documentation, and validate with your own runtime and logging configuration.
Reference and attribution
- Original post: https://www.devopsschool.com/blog/junit-4-test-logging-tips-using-slf4j/
- Referenced by the original author: http://www.catosplace.net/
Best practices
- Prefer SLF4J parameterized messages over string concatenation in log calls.
- Centralize repeated test lifecycle logs in a single rule to keep test bodies focused on assertions.
- Log start, pass, and fail events if CI diagnostics are important.
- Keep logging at appropriate levels (
infofor lifecycle,errorfor failures). - Confirm which JUnit 4 rule API your codebase standardizes on before refactoring.
I’m Rajesh Kumar, a DevOps, SRE, DevSecOps, Cloud, and Platform Engineering expert passionate about sharing practical knowledge, real-world experiences, and industry best practices. I have worked at Cotocus and regularly write about technology, travel, investing, health, product reviews, and digital marketing through my various platforms.
I publish technical articles at DevOps School, travel stories at Holiday Landmark, stock market insights at Stocks Mantra, health and fitness guidance at My Medic Plus, product reviews at TrueReviewNow, and SEO and digital marketing strategies at Wizbrand.
Find Trusted Cardiac Hospitals
Compare heart hospitals by city and services — all in one place.
Explore Hospitals