• Stars
    star
    366
  • Rank 116,547 (Top 3 %)
  • Language
    Java
  • License
    Apache License 2.0
  • Created almost 5 years ago
  • Updated about 1 month ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

๐ŸŽฏ LogCaptor captures log entries for unit and integration testing purposes

Actions Status Quality Gate Status Coverage Reliability Rating Security Rating Vulnerabilities Apache2 license Maven Central javadoc FOSSA Status Join the chat at https://gitter.im/hakky54/logcaptor

SonarCloud

LogCaptor Tweet

Install library with:

Install with maven

<dependency>
    <groupId>io.github.hakky54</groupId>
    <artifactId>logcaptor</artifactId>
    <version>2.9.0</version>
    <scope>test</scope>
</dependency>

Install with Gradle

testImplementation 'io.github.hakky54:logcaptor:2.9.0'

Install with Scala SBT

libraryDependencies += "io.github.hakky54" % "logcaptor" % "2.9.0" % Test

Install with Apache Ivy

<dependency org="io.github.hakky54" name="logcaptor" rev="2.9.0" />

Table of contents

  1. Introduction
  2. Usage
  3. Known issues
  4. Contributing
  5. License

Introduction

Hey, hello there ๐Ÿ‘‹ Welcome. I hope you will like this library โค๏ธ

LogCaptor is a library which will enable you to easily capture logging entries for unit and integration testing purposes.

Do you want to capture the console output? Please have a look at ConsoleCaptor.

Advantages

  • No mocking required
  • No custom JUnit extension required
  • Plug & play

Supported Java versions

  • Java 8
  • Java 11+

Tested Logging libraries

  • SLFJ4
  • Logback
  • Java Util Logging
  • Apache Log4j
  • Apache Log4j2
  • Log4j with Lombok
  • Log4j2 with Lombok
  • SLFJ4 with Lombok
  • JBossLog with Lombok
  • Java Util Logging with Lombok
  • Spring Boot Starter Log4j2
  • Google Flogger

See the unit test LogCaptorShould for all the scenario's or checkout this project Java Tutorials which contains more isolated examples of the individual logging frameworks

Usage

Capture logs
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class FooService {

    private static final Logger LOGGER = LogManager.getLogger(FooService.class);

    public void sayHello() {
        LOGGER.info("Keyboard not responding. Press any key to continue...");
        LOGGER.warn("Congratulations, you are pregnant!");
    }

}
Unit test:
import static org.assertj.core.api.Assertions.assertThat;

import nl.altindag.log.LogCaptor;
import org.junit.jupiter.api.Test;

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!"
                );
    }
}
Initialize LogCaptor once and reuse it during multiple tests with clearLogs method within the afterEach method:
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;
    private static final String EXPECTED_INFO_MESSAGE = "Keyboard not responding. Press any key to continue...";
    private static final String EXPECTED_WARN_MESSAGE = "Congratulations, you are pregnant!";
    
    @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 logInfoAndWarnMessagesAndGetWithEnum() {
        FooService service = new FooService();
        service.sayHello();

        assertThat(logCaptor.getInfoLogs()).containsExactly(EXPECTED_INFO_MESSAGE);
        assertThat(logCaptor.getWarnLogs()).containsExactly(EXPECTED_WARN_MESSAGE);

        assertThat(logCaptor.getLogs()).hasSize(2);
    }

    @Test
    public void logInfoAndWarnMessagesAndGetWithString() {
        FooService service = new FooService();
        service.sayHello();

        assertThat(logCaptor.getInfoLogs()).containsExactly(EXPECTED_INFO_MESSAGE);
        assertThat(logCaptor.getWarnLogs()).containsExactly(EXPECTED_WARN_MESSAGE);

        assertThat(logCaptor.getLogs()).hasSize(2);
    }

}
Class which will log events if specific log level has been set
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class FooService {

    private static final Logger LOGGER = LogManager.getLogger(FooService.class);

    public void sayHello() {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Keyboard not responding. Press any key to continue...");
        }
        LOGGER.info("Congratulations, you are pregnant!");
    }

}
Unit test:
import static org.assertj.core.api.Assertions.assertThat;

import nl.altindag.log.LogCaptor;
import org.junit.jupiter.api.Test;

public class FooServiceShould {

    @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())
            .doesNotContain("Keyboard not responding. Press any key to continue...")
            .isEmpty();
    }
}
Class which will also log an exception
import nl.altindag.log.service.Service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;

public class FooService {

    private static final Logger LOGGER = LoggerFactory.getLogger(ZooService.class);

    @Override
    public void sayHello() {
        try {
            tryToSpeak();
        } catch (IOException e) {
            LOGGER.error("Caught unexpected exception", e);
        }
    }

    private void tryToSpeak() throws IOException {
        throw new IOException("KABOOM!");
    }
}
Unit test:
import static org.assertj.core.api.Assertions.assertThat;

import nl.altindag.log.LogCaptor;
import nl.altindag.log.model.LogEvent;
import org.junit.jupiter.api.Test;

public class FooServiceShould {

    @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);
    }
}
Capture Managed Diagnostic Context (MDC)
import nl.altindag.log.service.LogMessage;
import nl.altindag.log.service.Service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

public class FooService {

    private static final Logger LOGGER = LoggerFactory.getLogger(ServiceWithSlf4jAndMdcHeaders.class);

    public void sayHello() {
        try {
            MDC.put("my-mdc-key", "my-mdc-value");
            LOGGER.info(LogMessage.INFO.getMessage());
        } finally {
            MDC.clear();
        }

        LOGGER.info("Hello there!");
    }

}
Unit test:
import static org.assertj.core.api.Assertions.assertThat;

import nl.altindag.log.LogCaptor;
import nl.altindag.log.model.LogEvent;
import org.junit.jupiter.api.Test;

public class FooServiceShould {

   @Test
   void captureLoggingEventsContainingMdc() {
      LogCaptor logCaptor = LogCaptor.forClass(FooService.class);

      FooService service = new FooService();
      service.sayHello();

      List<LogEvent> logEvents = logCaptor.getLogEvents();

      assertThat(logEvents).hasSize(2);

      assertThat(logEvents.get(0).getDiagnosticContext())
              .hasSize(1)
              .extractingByKey("my-mdc-key")
              .isEqualTo("my-mdc-value");

      assertThat(logEvents.get(1).getDiagnosticContext()).isEmpty();
   }
}
Disable any logs for a specific class

In some use cases a unit test can generate too many logs by another class. This could be annoying as it will cause noise in your build logs. LogCaptor can disable those log messages with the following snippet:

import static org.assertj.core.api.Assertions.assertThat;

import nl.altindag.log.LogCaptor;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

public class FooServiceShould {

    private static LogCaptor logCaptorForSomeOtherService = LogCaptor.forClass(SomeService.class);

    @BeforeAll
    static void disableLogs() {
        logCaptorForSomeOtherService.disableLogs();
    }

    @AfterAll
    static void resetLogLevel() {
        logCaptorForSomeOtherService.resetLogLevel();
    }

    @Test
    void logInfoAndWarnMessages() {
        LogCaptor logCaptor = LogCaptor.forClass(FooService.class);

        FooService service = new FooService();
        service.sayHello();

        assertThat(logCaptor.getLogs())
                .hasSize(2)
                .contains(
                    "Keyboard not responding. Press any key to continue...",
                    "Congratulations, you are pregnant!"
                );
    }
}
Disable console output

The console output can be disabled with 2 different options:

  1. With the LogCaptor.disableConsoleOutput() method
  2. With a logback-test.xml file, see below for the details.

Add logback-test.xml to your test resources with the following content:

<configuration>
   <statusListener class="ch.qos.logback.core.status.NopStatusListener" />
</configuration>
Returnable values from LogCaptor
import nl.altindag.log.LogCaptor;
import nl.altindag.log.model.LogEvent;

import java.time.ZonedDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import org.junit.jupiter.api.Test;

class FooServiceShould {

   @Test
   void showCaseAllReturnableValues() {
        LogCaptor logCaptor = LogCaptor.forClass(FooService.class);

        List<String> logs = logCaptor.getLogs();
        List<String> infoLogs = logCaptor.getInfoLogs();
        List<String> debugLogs = logCaptor.getDebugLogs();
        List<String> warnLogs = logCaptor.getWarnLogs();
        List<String> errorLogs = logCaptor.getErrorLogs();
        List<String> traceLogs = logCaptor.getTraceLogs();

        LogEvent logEvent = logCaptor.getLogEvents().get(0);
        String message = logEvent.getMessage();
        String formattedMessage = logEvent.getFormattedMessage();
        String level = logEvent.getLevel();
        List<Object> arguments = logEvent.getArguments();
        String loggerName = logEvent.getLoggerName();
        String threadName = logEvent.getThreadName();
        ZonedDateTime timeStamp = logEvent.getTimeStamp();
        Map<String, String> diagnosticContext = logEvent.getDiagnosticContext();
        Optional<Throwable> throwable = logEvent.getThrowable();
    }
    
}

Known issues

Using Log Captor alongside with other logging libraries

When building your maven or gradle project it can complain that you are using multiple SLF4J implementations. Log Captor is using logback as SLF4J implementation and SLF4J doesn't allow you to use multiple implementations, therefore you need to explicitly specify which to use during which build phase if you are using multiple SLF4J implementations.

During the test execution it can give you the following warning:

SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:~/.m2/repository/org/apache/logging/log4j/log4j-slf4j-impl/2.17.0/log4j-slf4j-impl-2.17.0.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:~/.m2/repository/ch/qos/logback/logback-classic/1.2.10/logback-classic-1.2.10.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.apache.logging.slf4j.Log4jLoggerFactory]

Because of this dependency issue the test may fail to capture the logs. You can fix that by excluding your main logging framework during the unit/integration test phase. Below is an example for Maven Failsafe and Maven Surefire. You can discover which dependency you need to exclude by anaylising the SLF4J warning displayed above.

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

And for gradle:

configurations {
    testImplementation {
        exclude(group = "org.apache.logging.log4j", module = "log4j-slf4j-impl")
        exclude(group = "org.apache.logging.log4j", module = "log4j-slf4j2-impl")
    }
}

Capturing logs of static inner classes

LogCaptor successfully captures logs of static inner classes with LogCaptor.forClass(MyStaticInnerClass.class) construction when SLF4J, Log4J, JUL is used. This doesn't apply to static inner classes when Log4J2 is used, because it uses different method to for initializing the logger. It used Class.getCanonicalName() under the covers instead of Class.getName(). You should use LogCaptor.forName(StaticInnerClass.class.getCanonicalName()) or LogCaptor.forRoot()for to be able to capture logs for static inner classes while using Log4J2.

Mixing up different classloaders

It may occur that different classloaders are being used during your tests. LogCaptor works with different classloaders, but it will fail to set up if part of it has been created with a different classloader. This may occur for example while using @QuarkusTest annotation. The logger will be setup by the default JDK classloader [jdk.internal.loader.ClassLoaders$AppClassLoader] while LogCaptor will be setup by the other classloader, in this case Quarkus [io.quarkus.bootstrap.classloading.QuarkusClassLoader]. LogCaptor will try to cast an object during the preparation, but it will fail as it is not possible to cast an object created by a different classloader. You need to make sure the logger is using the same classloader as LogCaptor or the other way around.

There is also an easier alternative solution by sending all of your logs to the console and capture that with ConsoleCaptor. Add the following two dependencies to your project:

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

Add the logback-test.xml configuration below to your test resources:

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

Your target class:

@Path("/hello")
public class HelloResource {
    
    Logger logger = LoggerFactory.getLogger(GreetingResource.class);

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        logger.info("Hello");
        return "Hello";
    }
}

Your test class:

import io.quarkus.test.junit.QuarkusTest;
import nl.altindag.console.ConsoleCaptor;
import org.junit.jupiter.api.Test;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

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

}

Contributing

There are plenty of ways to contribute to this project:

  • Give it a star
  • Share it with a Tweet
  • Join the Gitter room and leave a feedback or help with answering users questions
  • Submit a PR

License

FOSSA Status

More Repositories

1

certificate-ripper

๐Ÿ” A CLI tool to extract server certificates
Java
705
star
2

mutual-tls-ssl

๐Ÿ” Tutorial of setting up Security for your API with one way authentication with TLS/SSL and mutual authentication for a java based web server and a client with both Spring Boot. Different clients are provided such as Apache HttpClient, OkHttp, Spring RestTemplate, Spring WebFlux WebClient Jetty and Netty, the old and the new JDK HttpClient, the old and the new Jersey Client, Google HttpClient, Unirest, Retrofit, Feign, Methanol, vertx, Scala client Finagle, Featherbed, Dispatch Reboot, AsyncHttpClient, Sttp, Akka, Requests Scala, Http4s Blaze, Kotlin client Fuel, http4k, Kohttp and ktor. Also other server examples are available such as jersey with grizzly. Also gRPC, WebSocket and ElasticSearch examples are included
Java
567
star
3

sslcontext-kickstart

๐Ÿ” A lightweight high level library for configuring a http client or server based on SSLContext or other properties such as TrustManager, KeyManager or Trusted Certificates to communicate over SSL TLS for one way authentication or two way authentication provided by the SSLFactory. Support for Java, Scala and Kotlin based clients with examples. Available client examples are: Apache HttpClient, OkHttp, Spring RestTemplate, Spring WebFlux WebClient Jetty and Netty, the old and the new JDK HttpClient, the old and the new Jersey Client, Google HttpClient, Unirest, Retrofit, Feign, Methanol, Vertx, Scala client Finagle, Featherbed, Dispatch Reboot, AsyncHttpClient, Sttp, Akka, Requests Scala, Http4s Blaze, Kotlin client Fuel, http4k Kohttp and Ktor. Also gRPC, WebSocket and ElasticSearch examples are included
Java
499
star
4

java-tutorials

๐Ÿ“ A repository containing different java tutorials
Java
36
star
5

console-captor

๐ŸŽฏ ConsoleCaptor captures console output for unit and integration testing purposes
Java
30
star
6

stream-deck-profile

Elgato Stream Deck Profile
20
star
7

gatekeeper

๐Ÿ” A lightweight java library which guards your publicly accessible internal implementations.
Java
7
star
8

resize-me

An image-resizer app in Java 17, JavaFx and with afterburner framework
Java
5
star
9

jiggler

๐Ÿช„ Mouse Jiggler for you!
Java
4
star
10

ssl-server

Easily setup a SSL Server for testing purposes
Java
2
star
11

homebrew-crip

Ruby
2
star
12

welk-lidwoord

Een app die je helpt met kiezen van het juiste lidwoord
Java
1
star
13

Hakky54

1
star
14

hakky54.github.io

HTML
1
star
15

trust-me

Demo GUI for prompting to trust a server certificate
Java
1
star
16

yaslf4j

yet another simple logging facade for java
Java
1
star
17

random-number

Simple JavaFx app in Java 11 with Spring boot as dependency injection framwork
Java
1
star
18

product-status-checker

Tired of manually checking if a product is available? This lightweight application will take care of that.
Java
1
star