LogCaptor Documentation

repository·master·Indexed 19 days ago

https://github.com/hakky54/log-captor

A 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.

Tokens
4.7K
Snippets
11
Records
15
Agent score
16%

What's inside LogCaptor

  1. Overview of LogCaptor

    master
    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.
  2. Supported Java and Environment Compatibility

    master

    LogCaptor 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
  3. Capture logs across different threads

    master
    LogCaptor supports capturing logs emitted from different threads. To ensure successful capture, instantiate the LogCaptor as a local variable within your test method rather than as a static or instance variable.
  4. Handle classloader mismatches (e.g., Quarkus) using ConsoleCaptor

    master

    LogCaptor 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 consolecaptor and logback-classic to your test scope.

    2. Configure Logback

    Add a logback-test.xml to your test resources to route logs to STDOUT.

    3. Use ConsoleCaptor in Tests

    Wrap your test logic in a try-with-resources block using ConsoleCaptor and assert against getStandardOutput().

    <!-- 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");
           }
        }
    }
  5. Install LogCaptor

    master

    LogCaptor can be installed using various build tools. Ensure you use the test scope 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" % Test

    Install with Apache Ivy

    <dependency org="io.github.hakky54" name="logcaptor" rev="2.12.6" />
  6. Reuse LogCaptor across multiple tests

    master

    For better performance, you can initialize a LogCaptor once in a @BeforeAll method and reuse it across all tests in a class. To prevent logs from one test leaking into another, call logCaptor.clearLogs() in an @AfterEach method. Finally, ensure you call logCaptor.close() in an @AfterAll method 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
        }
    }
  7. Disable console output

    master

    You can prevent logs from appearing in the console using one of two methods:

    1. Call LogCaptor.disableConsoleOutput().
    2. Add a logback-test.xml file to your test resources with a NopStatusListener to suppress status messages.
    <configuration>
       <statusListener class="ch.qos.logback.core.status.NopStatusListener" />
    </configuration>
  8. Capture logs of static inner classes with Log4J2

    master

    While LogCaptor.forClass(MyStaticInnerClass.class) works for SLF4J, Log4J, and JUL, it fails for static inner classes when using Log4J2. This is because Log4J2 uses Class.getCanonicalName() instead of Class.getName() for initialization.

    To capture logs for static inner classes in Log4J2, use either LogCaptor.forName() with the canonical name or LogCaptor.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();
  9. Resolve SLF4J multiple bindings conflicts

    master

    LogCaptor 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 bindings warning 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")
        }
    }
  10. Capture and assert log messages

    master

    To 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 using getLogs(). 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!"
                    );
        }
    }
  11. Set log level dynamically during tests

    master

    You 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 by if (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();
    }
  12. Capture log events and exceptions

    master

    To 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 of LogEvent objects.

    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);
    }