Mastering Log Levels in Spring Boot Tests: A Practical Guide
Learn how to control log levels in Spring Boot tests with practical examples, production insights, and debugging strategies.
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
- ✓Java 17+ installed
- ✓Spring Boot 3.x project with spring-boot-starter-test dependency
- ✓Basic understanding of JUnit 5 and Spring Boot testing
• Use @SpringBootTest with spring.profiles.active=test and application-test.properties to set log levels per package. • Override log levels in test-specific logback-test.xml or via @MockBean for noisy dependencies. • Leverage OutputCaptureExtension from spring-boot-test to assert log output in unit tests. • Avoid System.out.println in tests; use a dedicated test logger with level control. • For integration tests, set org.springframework.boot.test to WARN to reduce noise.
Think of log levels like a volume knob on a radio. In production, you keep the volume low (WARN/ERROR) to avoid hearing every tiny detail. But during a test, you might want to crank it up to DEBUG to hear what's happening inside your code. Spring Boot lets you twist that knob per package, per test class, or even per method — without touching your production settings.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
Logging is the unsung hero of debugging. In 15 years of building payment-processing systems and SaaS billing platforms, I've seen more production outages caused by misconfigured logging than by actual code bugs. Spring Boot, with its auto-configuration and Logback integration, makes logging easy — but only if you know how to wield it. In tests, the stakes are different: you need visibility without drowning in noise. This guide covers everything from basic log level configuration to advanced patterns for integration tests with Testcontainers, WebClient, and reactive stacks. We'll use real Java code, production war stories, and the exact Spring Boot 3.x APIs you need. By the end, you'll know how to set log levels per test, assert log output, and avoid the common pitfalls that waste hours in CI pipelines.
Understanding Log Levels in Spring Boot
Spring Boot uses SLF4J as its logging facade and defaults to Logback. Log levels — TRACE, DEBUG, INFO, WARN, ERROR — control verbosity. In production, you typically set the root logger to INFO or WARN to avoid performance hits. But in tests, you often need DEBUG to diagnose flaky tests or network calls. Spring Boot's auto-configuration reads logging.level.* properties from application.properties or YAML. For example, logging.level.com.example.payment=DEBUG sets the level for the payment package. In tests, you can override these in application-test.properties or via @TestPropertySource. The key insight: log levels are hierarchical — a package-level setting overrides the root logger. This is critical when testing layered services like controllers, services, and repositories. I've seen teams set the root to DEBUG and wonder why their CI logs are 500MB — always scope levels to specific packages.
What the Official Docs Won't Tell You
The Spring Boot docs tell you how to set log levels via properties, but they don't warn you about the silent pitfalls. First, @SpringBootTest loads the full application context, which means your log configuration is merged from multiple sources: application.properties, application-test.properties, and logback-test.xml if present. The order matters: logback-test.xml overrides properties files. Second, when using @WebMvcTest or @DataJpaTest, the sliced context may ignore your custom logback-test.xml. I've spent hours debugging why a test showed no logs despite setting logging.level.com.example=DEBUG — turns out, the sliced test didn't load the logback file. The fix: use @TestPropertySource(properties = "logging.level.com.example=DEBUG") directly on the test class. Third, OutputCaptureExtension from spring-boot-test is your best friend for asserting logs, but it captures only logs written via SLF4J — not System.out or low-level logging frameworks. Always use SLF4J in your code.
Configuring Logback for Tests
Logback is the default logging framework in Spring Boot. For tests, you can create a src/test/resources/logback-test.xml file. This file overrides the default configuration when the test classpath is used. A common pattern is to set a different appender for tests, like a ConsoleAppender with a pattern that includes thread names and timestamps for debugging. You can also use a ThresholdFilter to filter logs by level. For example, you might want to capture all TRACE-level logs to a file for post-mortem analysis. But beware: if you have logback-spring.xml in src/main/resources, it won't be used in tests — Spring Boot's profile-specific configuration (logback-spring.xml) is not loaded in tests unless you explicitly set a profile. This is a classic gotcha. I always recommend using logback-test.xml for tests and keeping it simple: a single ConsoleAppender with a pattern that includes %-5level and %logger{36}.
Dynamic Log Level Changes in Tests
Sometimes you need to change log levels dynamically during a test — for example, to enable DEBUG for a specific method or to suppress logs from a noisy dependency. Spring Boot's LoggingSystem API allows programmatic changes via LoggingSystem.setLogLevel(). But in tests, you can use a simpler approach: @SpringBootTest with a custom test configuration that overrides log levels. For dynamic changes within a test method, use the ch.qos.logback.classic.Logger API. However, this couples your test to Logback. A better pattern is to use Spring's LoggingSystem abstraction. I've used this in integration tests for a WebClient-based service where the underlying Netty logs were too verbose. By setting reactor.netty to WARN at the start of the test and restoring it at the end, we kept the test output clean. Always restore log levels in @AfterEach to avoid polluting other tests.
Asserting Log Output in Tests
Asserting log output is a powerful technique for verifying that your code logs the right messages at the right levels. Spring Boot provides OutputCaptureExtension for JUnit 5, which captures all log output via SLF4J. You can use CapturedOutput.getAll() to get the entire log, or CapturedOutput.getOut() and getErr() for stdout and stderr. For more granular assertions, use Hamcrest or AssertJ matchers. For example, you can assert that a log contains a specific string, or that it contains a log at a specific level. However, OutputCaptureExtension has limitations: it captures logs from the entire test, including Spring Boot startup. To filter, use getAll() and then parse manually, or use a custom appender. I prefer to use @ExtendWith(OutputCaptureExtension.class) and then assert on the captured output. This is especially useful for testing error handling paths where you expect a specific WARN or ERROR log.
Log Levels in Integration Tests with Testcontainers
Integration tests with Testcontainers often produce a lot of noise from container startup, database drivers, and network calls. For example, when testing a PostgreSQL-backed service, the Postgres JDBC driver logs at DEBUG by default, flooding your test output. The solution: set logging level for the driver package to WARN. Additionally, Testcontainers itself logs at INFO, which can be useful for debugging container startup issues. I recommend setting org.testcontainers to INFO and the database driver to WARN in your test properties. For even finer control, you can use a logback-test.xml that applies different levels per test class. I've also used @TestPropertySource to override log levels for specific integration tests that need to debug database queries. The key is to balance visibility with noise — you want to see test failures, not container logs.
Log Levels in Reactive Tests
Reactive applications with Spring WebFlux and Reactor introduce additional logging complexity. Reactor's Flux and Mono logs are verbose at DEBUG, especially when using operators like flatMap or delayElements. In tests, you often need to see the reactive flow without drowning in reactor.core logs. The key is to set reactor.core to WARN and enable DEBUG only for your application code. Additionally, when using StepVerifier from reactor-test, you can use log() operator to print signals at a specific level. But beware: log() uses the global logger by default, which can be overridden. I recommend using log("com.example.reactive", Level.FINE) to control the log level. Another common pattern is to use @TestPropertySource to set logging.level.reactor.core=WARN for all reactive tests. This keeps the output clean and focused on your business logic.
Advanced Patterns: Conditional Logging and AOP
For complex test scenarios, you might need conditional logging — for example, log only if a test fails, or log specific parameters only when debugging. Spring AOP can be used to create a test aspect that logs method entry and exit with parameters. This is useful for integration tests where you want to trace the flow without adding log statements to production code. Another pattern is to use a custom test listener that changes log levels based on test status. For instance, if a test fails, you can enable DEBUG for the failing package and re-run the test. I've implemented this using TestExecutionListener from JUnit 5. However, this is advanced and often overkill. For most cases, OutputCaptureExtension and dynamic log level changes suffice. The real power comes from combining these techniques: use AOP for tracing, OutputCapture for assertions, and dynamic levels for isolation.
The Silent Payment Failure: When DEBUG Logs Masked a Production Outage
- Default log levels in tests can hide critical errors — always verify what you're suppressing.
- Use OutputCaptureExtension to assert that expected error logs appear during integration tests.
- Never assume a passing test means the system is healthy; log-level masking is a silent killer.
LoggingSystem.setLogLevel() and not directly manipulating Logback's Logger. Restore levels in @AfterEach.logging.level.com.example=DEBUGVerify logger is SLF4J: LoggerFactory.getLogger(getClass())| File | Command / Code | Purpose |
|---|---|---|
| application-test.properties | logging.level.root=WARN | Understanding Log Levels in Spring Boot |
| PaymentServiceTest.java | @ExtendWith(OutputCaptureExtension.class) | What the Official Docs Won't Tell You |
| logback-test.xml | Configuring Logback for Tests | |
| DynamicLogLevelTest.java | @SpringBootTest | Dynamic Log Level Changes in Tests |
| LogAssertionTest.java | @ExtendWith(OutputCaptureExtension.class) | Asserting Log Output in Tests |
| application-integration.properties | logging.level.org.testcontainers=INFO | Log Levels in Integration Tests with Testcontainers |
| ReactiveLogTest.java | @SpringBootTest | Log Levels in Reactive Tests |
| LoggingAspectTest.java | @Aspect | Advanced Patterns |
Key takeaways
Interview Questions on This Topic
How does Spring Boot resolve log levels from multiple configuration sources?
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
That's Spring Boot. Mark it forged?
4 min read · try the examples if you haven't