Skip to content

Commit

Permalink
add ExpectedKeyValue assertions
Browse files Browse the repository at this point in the history
  • Loading branch information
cleaning-agent committed Nov 24, 2023
1 parent 0d7cfe7 commit 62c838e
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 1 deletion.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
</developers>

<properties>
<project.version>3.5.0-SNAPSHOT</project.version>
<project.version>4.0.0-SNAPSHOT</project.version>

<java.version>17</java.version>
<lombok.version>1.18.30</lombok.version>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public synchronized void doAppend(ILoggingEvent loggingEvent) {
.mdcData(loggingEvent.getMDCPropertyMap())
.loggedException(getLoggedException(loggingEvent.getThrowableProxy()))
.markers(loggingEvent.getMarkerList())
.keyValuePairs(loggingEvent.getKeyValuePairs())
.build());
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package de.dm.infrastructure.logcapture;

import java.util.Objects;
import java.util.stream.Collectors;

import static java.lang.String.format;
import static java.lang.System.lineSeparator;

/**
* define expected key-value pair to be attached to a log message
*/
public final class ExpectedKeyValue implements LogEventMatcher {
private final String key;
private final Object value;

private ExpectedKeyValue(String key, Object value) {
if (key == null || value == null) {
throw new IllegalArgumentException("key and value are required for key-value log assertion");
}
this.key = key;
this.value = value;
}

@Override
public boolean matches(LoggedEvent loggedEvent) {
return loggedEvent.getKeyValuePairs() != null &&
loggedEvent.getKeyValuePairs().stream()
.anyMatch(pair -> key.equals(pair.key) && (
Objects.equals(value, pair.value) ||
areEqualAsNumbers(value, pair.value)
));
}

/*
* this is only done for Numbers because
* 1. toString() should not be expensive for these
* 2. when Logging 2L can be considered equal to 2, for example, but maybe not to 2.0
*/
private boolean areEqualAsNumbers(Object expectedValue, Object actualValue) {
return expectedValue instanceof Number && actualValue instanceof Number &&
expectedValue.toString().equals(actualValue.toString());
}

@Override
public String getNonMatchingErrorMessage(LoggedEvent loggedEvent) {
String expected = format(" expected key-value pair (%s, %s)", key, value) + lineSeparator();
return expected + format(" actual pairs: [%s]", loggedEvent.getKeyValuePairs() == null ? "" :
loggedEvent.getKeyValuePairs().stream()
.map(pair -> "(%s, %s)".formatted(pair.key, pair.value))
.collect(Collectors.joining(", ")));
}

@Override
public String getMatcherTypeDescription() {
return "key-value pair";
}

@Override
public String getMatcherDetailDescription() {
return format("key-value pair (%s, %s)", key, value);
}

/**
* use this in a log expectation to verify that something has been logged with a certain key-value pair
*
* @param key expected key
* @param value expected value
*
* @return expected key-value to use in log expectation
*/
public static ExpectedKeyValue keyValue(String key, Object value) {
return new ExpectedKeyValue(key, value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import lombok.Builder;
import lombok.Getter;
import org.slf4j.Marker;
import org.slf4j.event.KeyValuePair;

import java.util.List;
import java.util.Map;
Expand All @@ -25,6 +26,7 @@ public class LoggedEvent {
private final Optional<LoggedException> loggedException;
private final String loggerName;
private final List<Marker> markers;
private final List<KeyValuePair> keyValuePairs;

@SuppressWarnings("squid:S2166") // LoggedException is not an Exception, but the name is still appropriate
@AllArgsConstructor(access = PRIVATE)
Expand Down
138 changes: 138 additions & 0 deletions src/test/java/com/example/app/ReadableApiTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;

import java.math.BigDecimal;
import java.util.concurrent.atomic.AtomicInteger;

import static de.dm.infrastructure.logcapture.ExpectedException.exception;
import static de.dm.infrastructure.logcapture.ExpectedKeyValue.keyValue;
import static de.dm.infrastructure.logcapture.ExpectedLoggerName.logger;
import static de.dm.infrastructure.logcapture.ExpectedMarker.marker;
import static de.dm.infrastructure.logcapture.ExpectedMdcEntry.mdc;
Expand All @@ -21,6 +25,7 @@
import static de.dm.infrastructure.logcapture.LogExpectation.warn;
import static java.lang.System.lineSeparator;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;

@Slf4j
Expand Down Expand Up @@ -718,4 +723,137 @@ void combinedLogExpectationsOnlyOutputMismatch() {
lineSeparator() + " actual exception: message: \"an exception that was logged\", message: java.lang.RuntimeException" +
lineSeparator());
}

@Nested
class ExpectedKeyValues {
@Test
void failsWithDetailsNotMatchingExistingKeyValues() {
log.atInfo().setMessage("hello")
.addKeyValue("key1", 1)
.addKeyValue("key2", "value2")
.log();

var assertionError = assertThrows(AssertionError.class, () ->
logCapture.assertLogged(info("hello", keyValue("key", "a value")))
);
assertThat(assertionError).hasMessage("""
Expected log message has occurred, but never with the expected key-value pair: Level: INFO, Regex: "hello"
expected key-value pair (key, a value)
actual pairs: [(key1, 1), (key2, value2)]
""");
}

@Test
void failsWithDetailsWithNoExistingKeyValues() {
log.atInfo().setMessage("hello")
.log();

var assertionError = assertThrows(AssertionError.class, () ->
logCapture.assertLogged(info("hello", keyValue("key", "a value")))
);
assertThat(assertionError).hasMessage("""
Expected log message has occurred, but never with the expected key-value pair: Level: INFO, Regex: "hello"
expected key-value pair (key, a value)
actual pairs: []
""");
}

@Test
void requiresKey() {
var assertionError = assertThrows(IllegalArgumentException.class, () ->
logCapture.assertLogged(info("hello", keyValue(null, "a value")))
);
assertThat(assertionError).hasMessage("key and value are required for key-value log assertion");
}

@Test
void requiresValue() {
var assertionError = assertThrows(IllegalArgumentException.class, () ->
logCapture.assertLogged(info("hello", keyValue("a_key", null)))
);
assertThat(assertionError).hasMessage("key and value are required for key-value log assertion");
}

@Test
void succeedsWithString() {
log.atInfo().setMessage("hello")
.addKeyValue("name", "Frederick")
.log();

assertDoesNotThrow(() ->
logCapture.assertLogged(info("hello", keyValue("name", "Frederick")))
);
}

@Test
void succeedsWithLong() {
log.atInfo().setMessage("hello")
.addKeyValue("meaning", 42L)
.log();

assertDoesNotThrow(() ->
logCapture.assertLogged(info("hello", keyValue("meaning", 42L)))
);
}

@Test
void succeedsWithLongAndInt() {
log.atInfo().setMessage("hello")
.addKeyValue("meaning", 42)
.log();

assertDoesNotThrow(() ->
logCapture.assertLogged(info("hello", keyValue("meaning", 42L)))
);
}

@Test
void succeedsWithIntAndLong() {
log.atInfo().setMessage("hello")
.addKeyValue("meaning", 42L)
.log();

assertDoesNotThrow(() ->
logCapture.assertLogged(info("hello", keyValue("meaning", 42)))
);
}

@Test
void succeedsWithIntAndAtomicInt() {
log.atInfo().setMessage("hello")
.addKeyValue("meaning", new AtomicInteger(42))
.log();

assertDoesNotThrow(() ->
logCapture.assertLogged(info("hello", keyValue("meaning", 42)))
);
}

@Test
void failsWithBigDecimalAndPrecisionAndInt() {
log.atInfo().setMessage("hello")
.addKeyValue("meaning", new BigDecimal("42.00"))
.log();

var assertionError = assertThrows(AssertionError.class, () ->
logCapture.assertLogged(info("hello", keyValue("meaning", 42)))
);
assertThat(assertionError).hasMessage("""
Expected log message has occurred, but never with the expected key-value pair: Level: INFO, Regex: "hello"
expected key-value pair (meaning, 42)
actual pairs: [(meaning, 42.00)]
""");
}

@Test
void succeedsWithBigDecimalAndInt() {
log.atInfo().setMessage("hello")
.addKeyValue("meaning", new BigDecimal("42"))
.log();

assertDoesNotThrow(() ->
logCapture.assertLogged(info("hello", keyValue("meaning", 42)))
);
}
}
}

0 comments on commit 62c838e

Please sign in to comment.