Skip to content

Commit

Permalink
Merge pull request #7 from dm-drogeriemarkt/fluent-api
Browse files Browse the repository at this point in the history
fluent api
  • Loading branch information
dupps authored Apr 16, 2021
2 parents a91839a + 79fc21c commit f414b59
Show file tree
Hide file tree
Showing 6 changed files with 553 additions and 86 deletions.
124 changes: 77 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,70 +3,41 @@
[<img src="https://opensourcelogos.aws.dmtech.cloud/dmTECH_opensource_logo.svg" height="20" width="130">](https://dmtech.de/)
[![Build Status](https://travis-ci.org/dm-drogeriemarkt/log-capture.svg?branch=master)](https://travis-ci.org/dm-drogeriemarkt/log-capture)

Helper for Unit/Integration tests with JUnit 4/5 to test if something has been logged. See [Examples](#examples).
Simple assertions for log messages. See [Examples](#examples).

Because this is a library, Checkstyle is used to make sure all public classes/methods have appropriate Javadoc.
```java
logCapture
.info().assertLogged("hello world")
.warn().assertLogged("bye world");
```

**Table of Contents**

* [Changes](#changes)
* [3.1.0](#310)
* [3.0.0](#300)
* [2.0.1](#201)
* [Updating from Version 1.x.x to 2.x.x](#updating-from-version-1xx-to-2xx)
* [Usage](#usage)
* [Junit 4 vs 5](#junit-4-vs-5)
* [Maven](#maven)
* [Examples](#examples)
* [Unit Test Example:](#unit-test-example)
* [Integration Test Example:](#integration-test-example)
* [Example with MDC](#example-with-mdc)
* [Usage with non-JUnit Runner](#usage-with-non-junit-runner)
* [Usage outside of JUnit 5 (Cucumber example)](#usage-outside-of-junit-5-cucumber-example)
* [Cucumber example](#cucumber-example)
* [Cucumber feature file](#cucumber-feature-file)
* [Cucumber stepdefs](#cucumber-stepdefs)
* [Cucumber DTOs](#cucumber-dtos)


## Changes

### 3.1.0

Added `assertNothingElseLogged()`

### 3.0.0

Updated from JUnit 4 to JUnit 5.

To update from 2.x.x to 3.x.x:

* Use JUnit 5 instead of JUnit 4
* Replace `@Rule` in logging tests with `@RegisterExtension`

### 2.0.1

Fixed a bug where multiline log messages (for example Messages that contain a stack trace) could not be matched.

### Updating from Version 1.x.x to 2.x.x

* `LogCapture.forUnitTest()` has been replaced with `LogCapture.forCurrentPackage()`
* `LogCapture.forIntegrationTest(...)` has been replaced with `LogCapture.forPackages(...)`
* `logCapture.addAppender()` has been replaced with `logCapture.addAppenderAndSetLogLevelToDebug()`
* `logCapture.removeAppender()` has been replaced with `logCapture.removeAppenderAndResetLogLevel()`
* [Changes](#changes)
* [3.2.0](#320)
* [3.1.0](#310)
* [3.0.0](#300)
* [2.0.1](#201)
* [Updating from Version 1.x.x to 2.x.x](#updating-from-version-1xx-to-2xx)

## Usage

### Junit 4 vs 5

If you still use Junit 4, you need to use LogCapture 2.x.x

There is no guarantee, however, if and how long 2.x.x will be maintained. We plan to maintain it as long as it is needed, though.

### Maven

Add log-capture as a test dependency to your project. If you use Maven, add this to your pom.xml:

```pom.xml
```xml
<dependency>
<groupId>de.dm.infrastructure</groupId>
<artifactId>log-capture</artifactId>
Expand Down Expand Up @@ -97,6 +68,13 @@ public class MyUnitTest {
log.info("something interesting");
log.error("something terrible");

//assert that the messages have been logged
//expected log message is a regular expression
logCapture
.info().assertLogged("^something interesting$")
.error().assertLogged("terrible")

// alterative using the old, non-fluent API
logCapture
.assertLogged(Level.INFO, "^something interesting$") //second parameter is a regular expression
.thenLogged(Level.ERROR, "terrible")
Expand Down Expand Up @@ -131,9 +109,15 @@ public class MyIntegrationTest {
log.info("something interesting");
log.error("something terrible");

logCapture
.info().assertLogged("^something interesting$")
.info().assertLogged("^start of info from utility.that.logs") // no $ at the end to only match the start of the message
.error().("terrible");

// alterative using the old, non-fluent API
logCapture
.assertLogged(Level.INFO, "^something interesting")
.assertLogged(Level.INFO, "^info from utility.that.logs")
.assertLogged(Level.INFO, "^start of info from utility.that.logs")
.thenLogged(Level.ERROR, "terrible");
}
}
Expand Down Expand Up @@ -161,7 +145,22 @@ public class MyUnitTest {
MDC.put("my_mdc_key", "this is the MDC value");
MDC.put("other_mdc_key", "this is the other MDC value");
log.info("this message has some MDC information attached");
log.info("this message has some MDC information attached, too");

logCapture
.info()
.withMdc("my_mdc_key", "^this is the MDC value$")
.withMdc("other_mdc_key", "^this is the other MDC value$")
.assertLogged("information attached")

// to assert MDC content in both messages
logCapture
.withMdcForAll("my_mdc_key", "^this is the MDC value$")
.withMdcForAll("other_mdc_key", "^this is the other MDC value$")
.info().assertLogged("information attached")
.info().assertLogged("information attached, too")

// non-fluent API (no equivalent to withMdcForAll here)
logCapture
.assertLogged(Level.INFO, "information attached",
withMdc("my_mdc_key", "^this is the MDC value$"),
Expand Down Expand Up @@ -197,9 +196,9 @@ java.lang.AssertionError: Expected log message has occurred, but never with the
other_mdc_key: "this is the other MDC value"
```

## Usage with non-JUnit Runner
## Usage outside of JUnit 5 (Cucumber example)

If you intend to use LogCapture outside of a JUnit test, you cannot rely on JUnit's `@Rule` annotation and must call LocCapture's `addAppenderAndSetLogLevelToDebug()` and `removeAppenderAndResetLogLevel()` methods manually.
If you intend to use LogCapture outside of a JUnit test, you cannot rely on JUnit's `@Rule` annotation and must call LocCapture's `addAppenderAndSetLogLevelToTrace()` and `removeAppenderAndResetLogLevel()` methods manually.

Be aware that this will still cause JUnit to be a dependency.

Expand Down Expand Up @@ -231,7 +230,7 @@ public class LoggingStepdefs {

@Before
public void setupLogCapture() {
logCapture.addAppenderAndSetLogLevelToDebug();
logCapture.addAppenderAndSetLogLevelToTrace();
}

@After
Expand Down Expand Up @@ -281,3 +280,34 @@ public class LogEntry {
private String messageRegex;
}
```

## Changes

### 3.2.0

Added fluent API

### 3.1.0

Added `assertNothingElseLogged()`

### 3.0.0

Updated from JUnit 4 to JUnit 5.

To update from 2.x.x to 3.x.x:

* Use JUnit 5 instead of JUnit 4
* Replace `@Rule` in logging tests with `@RegisterExtension`

### 2.0.1

Fixed a bug where multiline log messages (for example Messages that contain a stack trace) could not be matched.

### Updating from Version 1.x.x to 2.x.x

* `LogCapture.forUnitTest()` has been replaced with `LogCapture.forCurrentPackage()`
* `LogCapture.forIntegrationTest(...)` has been replaced with `LogCapture.forPackages(...)`
* `logCapture.addAppender()` has been replaced with `logCapture.addAppenderAndSetLogLevelToTrace()`
* `logCapture.removeAppender()` has been replaced with `logCapture.removeAppenderAndResetLogLevel()`

141 changes: 141 additions & 0 deletions src/main/java/de/dm/infrastructure/logcapture/FluentLogAssertion.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package de.dm.infrastructure.logcapture;

import ch.qos.logback.classic.Level;
import de.dm.infrastructure.logcapture.LogCapture.LastCapturedLogEvent;
import lombok.RequiredArgsConstructor;

import java.util.LinkedList;
import java.util.List;
import java.util.Optional;

import static ch.qos.logback.classic.Level.DEBUG;
import static ch.qos.logback.classic.Level.ERROR;
import static ch.qos.logback.classic.Level.INFO;
import static ch.qos.logback.classic.Level.TRACE;
import static ch.qos.logback.classic.Level.WARN;
import static lombok.AccessLevel.PACKAGE;
import static lombok.AccessLevel.PRIVATE;

/**
* Helper class for fluent log assertions. Use this via {@link LogCapture#info()} et al. or {@link LogCapture#withMdcForAll(String, String)}
*/
@RequiredArgsConstructor(access = PACKAGE)
public class FluentLogAssertion {
private final LogCapture logCapture;
private final Optional<LastCapturedLogEvent> lastCapturedLogEvent;
private final List<ExpectedMdcEntry> expectedMdcEntries = new LinkedList<>();

/**
* prepare the assertion of log messages with MDC contents
*
* @param key MDC key
* @param regex regular expression describing the MDC value
*
* @return FluentLogAssertion to assert the messages with MDC
*/
public FluentLogAssertion withMdcForAll(String key, String regex) {
FluentLogAssertion newAssertion = new FluentLogAssertion(logCapture, lastCapturedLogEvent);
newAssertion.expectedMdcEntries.addAll(expectedMdcEntries);
newAssertion.expectedMdcEntries.add(ExpectedMdcEntry.withMdc(key, regex));

return newAssertion;
}

/**
* prepare the assertion of a logged warn message
*
* @return FluentLogAssertion to assert an warn message
*/
public ConfiguredLogAssertion warn() {
return new ConfiguredLogAssertion(WARN);
}

/**
* prepare the assertion of a logged error message
*
* @return FluentLogAssertion to assert an error message
*/
public ConfiguredLogAssertion error() {
return new ConfiguredLogAssertion(ERROR);
}

/**
* prepare the assertion of a logged info message
*
* @return FluentLogAssertion to assert an info message
*/
public ConfiguredLogAssertion info() {
return new ConfiguredLogAssertion(INFO);
}

/**
* prepare the assertion of a logged debug message
*
* @return FluentLogAssertion to assert an debug message
*/
public ConfiguredLogAssertion debug() {
return new ConfiguredLogAssertion(DEBUG);
}

/**
* prepare the assertion of a logged trace message
*
* @return FluentLogAssertion to assert an trace message
*/
public ConfiguredLogAssertion trace() {
return new ConfiguredLogAssertion(TRACE);
}

/**
* assert that nothing else has been logged
*/
public void assertNothingElseLogged() {
LastCapturedLogEvent presentEvent = lastCapturedLogEvent.orElseThrow(() ->
new IllegalStateException("assertNothingElseLogged() must be called with a previous log assertion"));
presentEvent.assertNothingElseLogged();
}

/**
* Helper class for fluent log assertions. Use this via {@link LogCapture#info()} et al. or {@link LogCapture#withMdcForAll(String, String)}
*/
@RequiredArgsConstructor(access = PRIVATE)
public class ConfiguredLogAssertion {
private final Level level;
private final List<ExpectedMdcEntry> expectedMdcEntries = new LinkedList<>(FluentLogAssertion.this.expectedMdcEntries);

/**
* configure the next message assertion to also assert MDC contents
*
* @param key MDC key
* @param regex regular expression describing the MDC value
*
* @return pre-configured assertion
*/
public ConfiguredLogAssertion withMdc(String key, String regex) {
ConfiguredLogAssertion newAssertion = new ConfiguredLogAssertion(level);
newAssertion.expectedMdcEntries.addAll(expectedMdcEntries);
newAssertion.expectedMdcEntries.add(ExpectedMdcEntry.withMdc(key, regex));

return newAssertion;
}

/**
* assert that a message has been logged, depending on previous configuration
*
* @param regex regex to match formatted log message (with Pattern.DOTALL and Pattern.MULTILINE)
*
* @return configuration to allow further assertions
*/
public FluentLogAssertion assertLogged(String regex) {
final LastCapturedLogEvent capturedLogEvent;
if (lastCapturedLogEvent.isPresent()) {
capturedLogEvent = lastCapturedLogEvent.get().thenLogged(level, regex, expectedMdcEntries.toArray(new ExpectedMdcEntry[0]));
} else {
capturedLogEvent = logCapture.assertLogged(level, regex, expectedMdcEntries.toArray(new ExpectedMdcEntry[0]));
}
FluentLogAssertion newFluentLogAssertion = new FluentLogAssertion(logCapture, Optional.of(capturedLogEvent));
newFluentLogAssertion.expectedMdcEntries.addAll(FluentLogAssertion.this.expectedMdcEntries);
return newFluentLogAssertion;
}
}
}
Loading

0 comments on commit f414b59

Please sign in to comment.