LogCaptor Documentation
repository·master·Indexed 19 days ago
https://github.com/hakky54/log-captorA lightweight, plug-and-play Java library for capturing and asserting log messages during unit and integration tests without using mocks or complex JUnit extensions. It supports Java 8, 11+, Kotlin 1.5+, Scala 2.11+, and Android API 24+, with compatibility for logging frameworks including SLF4J, Logback, JUL, Log4j, Log4j2, and Google Flogger.
What's inside LogCaptor
- LogCaptor is a plug-and-play library designed to capture logging entries for unit and integration testing. It eliminates the need for custom JUnit extensions, mocking, or complex configurations. It works by intercepting logs from various logging frameworks directly.
Supported Java and Environment Compatibility
masterLogCaptor supports the following environments:
- Java Versions: Java 8, Java 11+
- Kotlin: 1.5+
- Scala: 2.11+
- Android: API 24+
Tested Logging Libraries
LogCaptor has been tested with:
- SLF4J
- Logback
- Java Util Logging (JUL)
- Apache Log4j
- Apache Log4j2
- Google Flogger
- Sude
- Various combinations of the above with Lombok (e.g., Log4j2 with Lombok, SLF4J with Lombok)
- Spring Boot Starter Log4j2
- JBossLog with Lombok
Capture logs across different threads
masterLogCaptor supports capturing logs emitted from different threads. To ensure successful capture, instantiate theLogCaptoras a local variable within your test method rather than as a static or instance variable.Handle classloader mismatches (e.g., Quarkus) using ConsoleCaptor
masterLogCaptor may fail to set up if the logger and LogCaptor are initialized by different classloaders (a common issue in frameworks like Quarkus using
@QuarkusTest).An alternative solution is to redirect all logs to the console and use ConsoleCaptor to verify them.
1. Add Dependencies
Add
consolecaptorandlogback-classicto your test scope.2. Configure Logback
Add a
logback-test.xmlto your test resources to route logs toSTDOUT.3. Use ConsoleCaptor in Tests
Wrap your test logic in a try-with-resources block using
ConsoleCaptorand assert againstgetStandardOutput().<!-- 1. Dependencies --> <dependencies> <dependency> <groupId>io.github.hakky54</groupId> <artifactId>consolecaptor</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <scope>test</scope> </dependency> </dependencies> <!-- 2. logback-test.xml --> <configuration> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <root level="TRACE"> <appender-ref ref="STDOUT" /> </root> </configuration> <!-- 3. Test Implementation --> @QuarkusTest class QuarkusTestTest { @Test void captureLogs() { try(ConsoleCaptor consoleCaptor = new ConsoleCaptor()) { HelloResource resource = new HelloResource(); resource.hello(); List<String> standardOutput = consoleCaptor.getStandardOutput(); assertThat(standardOutput).hasSize(1).contains("Hello"); } } }Install LogCaptor
masterLogCaptor can be installed using various build tools. Ensure you use the
testscope so the library is only available during testing.### Install with [maven] ```xml <dependency> <groupId>io.github.hakky54</groupId> <artifactId>logcaptor</artifactId> <version>2.12.6</version> <scope>test</scope> </dependency>Install with Gradle
testImplementation 'io.github.hakky54:logcaptor:2.12.6'Install with Scala SBT
libraryDependencies += "io.github.hakky54" % "logcaptor" % "2.12.6" % TestInstall with Apache Ivy
<dependency org="io.github.hakky54" name="logcaptor" rev="2.12.6" />Reuse LogCaptor across multiple tests
masterFor better performance, you can initialize a
LogCaptoronce in a@BeforeAllmethod and reuse it across all tests in a class. To prevent logs from one test leaking into another, calllogCaptor.clearLogs()in an@AfterEachmethod. Finally, ensure you calllogCaptor.close()in an@AfterAllmethod to release resources.import nl.altindag.log.LogCaptor; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; public class FooServiceShould { private static LogCaptor logCaptor; @BeforeAll public static void setupLogCaptor() { logCaptor = LogCaptor.forClass(FooService.class); } @AfterEach public void clearLogs() { logCaptor.clearLogs(); } @AfterAll public static void tearDown() { logCaptor.close(); } @Test public void testMethod() { // ... test logic } }Disable console output
masterYou can prevent logs from appearing in the console using one of two methods:
- Call
LogCaptor.disableConsoleOutput(). - Add a
logback-test.xmlfile to your test resources with aNopStatusListenerto suppress status messages.
<configuration> <statusListener class="ch.qos.logback.core.status.NopStatusListener" /> </configuration>- Call
Capture logs of static inner classes with Log4J2
masterWhile
LogCaptor.forClass(MyStaticInnerClass.class)works for SLF4J, Log4J, and JUL, it fails for static inner classes when using Log4J2. This is because Log4J2 usesClass.getCanonicalName()instead ofClass.getName()for initialization.To capture logs for static inner classes in Log4J2, use either
LogCaptor.forName()with the canonical name orLogCaptor.forRoot().// Use forName with the canonical name for Log4J2 static inner classes LogCaptor.forName(StaticInnerClass.class.getCanonicalName()); // Or use forRoot to capture everything LogCaptor.forRoot();Resolve SLF4J multiple bindings conflicts
masterLogCaptor uses Logback as its SLF4J implementation. If your project already uses another SLF4J binding (like Log4j), you will see a
SLF4J: Class path contains multiple SLF4J bindingswarning during tests. This can prevent LogCaptor from capturing logs.To fix this, exclude your main logging framework's SLF4J implementation during the test phase. Identify the conflicting dependency from the SLF4J warning message (e.g.,
org.apache.logging.log4j:log4j-slf4j-impl).<!-- Maven Surefire/Failsafe Example --> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <configuration> <classpathDependencyExcludes> <classpathDependencyExclude>org.apache.logging.log4j:log4j-slf4j-impl</classpathDependencyExclude> <classpathDependencyExclude>org.apache.logging.log4j:log4j-slf4j2-impl</classpathDependencyExclude> </classpathDependencyExcludes> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-failsafe-plugin</artifactId> <configuration> <classpathDependencyExcludes> <classpathDependencyExclude>org.apache.logging.log4j:log4j-slf4j-impl</classpathDependencyExclude> <classpathDependencyExclude>org.apache.logging.log4j:log4j-slf4j2-impl</classpathDependencyExclude> </classpathDependencyExcludes> </configuration> </plugin> </plugins> </build> <!-- Gradle Example --> configurations { testImplementation { exclude(group = "org.apache.logging.log4j", module = "log4j-slf4j-impl") exclude(group = "org.apache.logging.log4j", module = "log4j-slf4j2-impl") } }Capture and assert log messages
masterTo capture logs for a specific class, use
LogCaptor.forClass(Class<?> clazz). You can then retrieve logs filtered by their severity level (e.g.,getInfoLogs(),getWarnLogs(),getErrorLogs(), etc.) or retrieve all logs usinggetLogs(). This is useful for verifying that specific messages were logged during a method execution.import nl.altindag.log.LogCaptor; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; public class FooServiceShould { @Test public void logInfoAndWarnMessages() { LogCaptor logCaptor = LogCaptor.forClass(FooService.class); FooService fooService = new FooService(); fooService.sayHello(); // Get logs based on level assertThat(logCaptor.getInfoLogs()).containsExactly("Keyboard not responding. Press any key to continue..."); assertThat(logCaptor.getWarnLogs()).containsExactly("Congratulations, you are pregnant!"); // Get all logs assertThat(logCaptor.getLogs()) .hasSize(2) .contains( "Keyboard not responding. Press any key to continue...", "Congratulations, you are pregnant!" ); } }Set log level dynamically during tests
masterYou can force a specific log level for the captured class using
logCaptor.setLogLevelTo[Level]()(e.g.,setLogLevelToInfo()). This is useful for testing code paths that are guarded byif (logger.isDebugEnabled())checks.@Test public void logInfoAndWarnMessages() { LogCaptor logCaptor = LogCaptor.forClass(FooService.class); logCaptor.setLogLevelToInfo(); FooService fooService = new FooService(); fooService.sayHello(); assertThat(logCaptor.getInfoLogs()).contains("Congratulations, you are pregnant!"); assertThat(logCaptor.getDebugLogs()).isEmpty(); }Capture log events and exceptions
masterTo inspect detailed metadata about a log entry, such as the exception thrown, the log level, or the thread name, use
logCaptor.getLogEvents(). This returns a list ofLogEventobjects.import nl.altindag.log.LogCaptor; import nl.altindag.log.model.LogEvent; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @Test void captureLoggingEventsContainingException() { LogCaptor logCaptor = LogCaptor.forClass(ZooService.class); FooService service = new FooService(); service.sayHello(); List<LogEvent> logEvents = logCaptor.getLogEvents(); assertThat(logEvents).hasSize(1); LogEvent logEvent = logEvents.get(0); assertThat(logEvent.getMessage()).isEqualTo("Caught unexpected exception"); assertThat(logEvent.getLevel()).isEqualTo("ERROR"); assertThat(logEvent.getThrowable()).isPresent(); assertThat(logEvent.getThrowable().get()) .hasMessage("KABOOM!") .isInstanceOf(IOException.class); }