Install with maven
<dependency>
<groupId>io.github.hakky54</groupId>
<artifactId>logcaptor</artifactId>
<version>2.7.8</version>
<scope>test</scope>
</dependency>
testImplementation 'io.github.hakky54:logcaptor:2.7.8'
libraryDependencies += "io.github.hakky54" % "logcaptor" % "2.7.8" % Test
<dependency org="io.github.hakky54" name="logcaptor" rev="2.7.8" />
Hey, hello there 👋 Welcome, you are I hope you will like this library ❤️
LogCaptor is a library which will enable you to easily capture logging entries for unit testing purposes.
Do you want to capture the console output? Please have a look at ConsoleCaptor.
- No mocking required
- No custom JUnit extension required
- Plug & play
- Java 8
- Java 11+
- SLFJ4
- Logback
- Java Util Logging
- Apache Log4j
- Apache Log4j2
- Log4j with Lombok
- Log4j2 with Lombok
- SLFJ4 with Lombok
- Java Util Logging with Lombok
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
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!");
}
}
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);
}
}
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!");
}
}
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();
}
}
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!");
}
}
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);
}
}
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!");
}
}
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();
}
}
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!"
);
}
}
Add logback-test.xml
to your test resources with the following content:
<configuration>
<statusListener class="ch.qos.logback.core.status.NopStatusListener" />
</configuration>
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();
}
}
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. 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:
<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>
</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>
</classpathDependencyExcludes>
</configuration>
</plugin>
</plugins>
</build>
And for gradle:
configurations {
testImplementation {
exclude group: 'org.apache.logging.log4j', module: 'log4j-slf4j-impl'
}
}
LogCaptor successfully catches logs of static inner classes with LogCaptor.forClass(StaticInnerClass.class)
construction
when SLF4J, Log4J, JUL is used. This doesn't apply to Log4J2 by default 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())
for successful
test execution with Log4j2.
There are plenty of ways to contribute to this project:
- Give it a star
- Share it with a
- Join the Gitter room and leave a feedback or help with answering users questions
- Submit a PR
Thanks goes to these wonderful people (emoji key):
Vasiliy Sobolev 🎨 🤔💻 |
Alexei Brinza 🎨💻 |
Ingyu Hwang 👀 🤔 |
Tomasz Juchniewicz 🤔 |
This project follows the all-contributors specification. Contributions of any kind welcome!