Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Differences support #59

Merged
merged 16 commits into from
Oct 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Changelog.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Changelog

## 4.6-SNAPSHOT
## 5.1-SNAPSHOT

## 5.0 (2022-10-04)
- Differences support

## 4.5
Do not include values inside assertion error message if JSONs are equal
Expand Down
80 changes: 53 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,37 +109,63 @@ JSONCompare.assertMatches(expected, actual, new JsonComparator() {
// should fail
```

## Error messages
## Differences
```javascript
String expected = "{\"b\":{\"x\":\"val1\",\"y\":\"val2\"},\"a\":{\"t\":\"val3\",\"z\":\"val1\"}}";
String actual = "{\"a\":{\"t\":\"val3\",\"z\":\"val4\"},\"b\":{\"x\":\"val1\",\"y\":\"val2\"}}";
JSONCompare.assertMatches(expected, actual);
String expected = "{\n" +
" \"caught\": false,\n" +
" \"pain\": {\n" +
" \"range\": [\n" +
" \"bell\",\n" +
" \"blue\",\n" +
" -2059921070\n" +
" ],\n" +
" \"not_anyone\": -1760889549.4041045,\n" +
" \"flat\": -2099670336\n" +
" }\n" +
"}";
String actual = "{\n" +
" \"caught\": true,\n" +
" \"pain\": {\n" +
" \"range\": [\n" +
" \"bell\",\n" +
" \"red\",\n" +
" -2059921075\n" +
" ],\n" +
" \"anyone\": -1760889549.4041045,\n" +
" \"flat\": -2099670336\n" +
" },\n" +
" \"broad\": \"invented\"\n" +
"}";
JSONCompare.assertMatches(expected, actual);


Output:

java.lang.AssertionError: Expected ["val1"] but found ["val4"] <- field "z" <- field "a"
Expected:
{
"b" : {
"x" : "val1",
"y" : "val2"
},
"a" : {
"t" : "val3",
"z" : "val1"
}
}
But got:
{
"a" : {
"t" : "val3",
"z" : "val4"
},
"b" : {
"x" : "val1",
"y" : "val2"
}
}
org.opentest4j.AssertionFailedError: FOUND 4 DIFFERENCE(S):


_________________________DIFF__________________________
caught ->
Expected value: false But got: true

_________________________DIFF__________________________
pain -> range ->
Expected element from position 2 was NOT FOUND:
"blue"

_________________________DIFF__________________________
pain -> range ->
Expected element from position 3 was NOT FOUND:
-2059921070

_________________________DIFF__________________________
pain -> Field 'not_anyone' was NOT FOUND


Json matching by default uses regular expressions.
In case expected json contains any unintentional regexes, then quote them between \Q and \E delimiters or use a custom comparator.
==>
<Click to see difference>
```

## Matching with some tweaks
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

<groupId>com.github.fslev</groupId>
<artifactId>json-compare</artifactId>
<version>4.6-SNAPSHOT</version>
<version>5.0-SNAPSHOT</version>

<dependencies>
<!--jackson-->
Expand Down
33 changes: 17 additions & 16 deletions src/main/java/io/json/compare/JSONCompare.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

import com.fasterxml.jackson.databind.JsonNode;
import io.json.compare.matcher.JsonMatcher;
import io.json.compare.matcher.MatcherException;
import io.json.compare.util.JsonUtils;
import org.junit.jupiter.api.AssertionFailureBuilder;

import java.io.IOException;
import java.util.List;
import java.util.Set;


Expand All @@ -16,7 +16,7 @@

public class JSONCompare {

private static final String ASSERTION_ERROR_HINT_MESSAGE = "Json matching by default uses regular expressions.\n" +
private static final String ASSERTION_ERROR_HINT_MESSAGE = "Json matching by default uses regular expressions." + System.lineSeparator() +
"In case expected json contains any unintentional regexes, then quote them between \\Q and \\E delimiters or use a custom comparator.";

public static void assertMatches(Object expected, Object actual) {
Expand Down Expand Up @@ -62,30 +62,31 @@ public static void assertNotMatches(Object expected, Object actual, Set<CompareM
public static void assertMatches(Object expected, Object actual, JsonComparator comparator, Set<CompareMode> compareModes, String message) {
JsonNode expectedJson = toJson(expected);
JsonNode actualJson = toJson(actual);
try {
new JsonMatcher(expectedJson, actualJson,
comparator == null ? new DefaultJsonComparator() : comparator, compareModes).match();
} catch (MatcherException e) {
String defaultMessage = String.format("%s\n", e.getMessage());
List<String> diffs = new JsonMatcher(expectedJson, actualJson,
comparator == null ? new DefaultJsonComparator() : comparator, compareModes).match();
if (!diffs.isEmpty()) {
String defaultMessage = String.format("FOUND %s DIFFERENCE(S):" + System.lineSeparator() + "%s" + System.lineSeparator(),
diffs.size(), diffs.stream().map(diff ->
System.lineSeparator() + System.lineSeparator() + "_________________________DIFF__________________________" +
System.lineSeparator() + diff).reduce(String::concat).get());
if (comparator == null || comparator.getClass().equals(DefaultJsonComparator.class)) {
defaultMessage += "\n\n" + ASSERTION_ERROR_HINT_MESSAGE + "\n";
defaultMessage += System.lineSeparator() + System.lineSeparator() + ASSERTION_ERROR_HINT_MESSAGE + System.lineSeparator();
}
AssertionFailureBuilder.assertionFailure().message(message == null ? defaultMessage : defaultMessage + "\n" + message)
AssertionFailureBuilder.assertionFailure().message(message == null ? defaultMessage : defaultMessage + System.lineSeparator() + message)
.expected(prettyPrint(expectedJson)).actual(prettyPrint(actualJson)).buildAndThrow();
}
}

public static void assertNotMatches(Object expected, Object actual, JsonComparator comparator, Set<CompareMode> compareModes, String message) {
JsonNode expectedJson = toJson(expected);
JsonNode actualJson = toJson(actual);
try {
new JsonMatcher(expectedJson, actualJson,
comparator == null ? new DefaultJsonComparator() : comparator, compareModes).match();
} catch (MatcherException e) {
List<String> diffs = new JsonMatcher(expectedJson, actualJson,
comparator == null ? new DefaultJsonComparator() : comparator, compareModes).match();
if (!diffs.isEmpty()) {
return;
}
String defaultMessage = "JSONs are equal";
AssertionFailureBuilder.assertionFailure().message(message == null ? defaultMessage : defaultMessage + "\n" + message)
String defaultMessage = System.lineSeparator() + "JSONs are equal";
AssertionFailureBuilder.assertionFailure().message(message == null ? defaultMessage : defaultMessage + System.lineSeparator() + message)
.expected(prettyPrint(expectedJson)).actual(prettyPrint(actualJson))
.includeValuesInMessage(false).buildAndThrow();
}
Expand All @@ -102,7 +103,7 @@ private static JsonNode toJson(Object obj) {
try {
return JsonUtils.toJson(obj);
} catch (IOException e) {
throw new RuntimeException(String.format("Invalid JSON\n%s\n", e));
throw new RuntimeException(String.format("Invalid JSON" + System.lineSeparator() + "%s" + System.lineSeparator(), e));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@
import io.json.compare.DefaultJsonComparator;
import io.json.compare.JsonComparator;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Optional;
import java.util.Set;
import java.util.*;

abstract class AbstractJsonMatcher {

Expand All @@ -27,7 +24,7 @@ abstract class AbstractJsonMatcher {
this.compareModes = compareModes == null ? new HashSet<>() : compareModes;
}

protected abstract void match() throws MatcherException;
protected abstract List<String> match();

protected static UseCase getUseCase(JsonNode node) {
if (node.isValueNode()) {
Expand Down
89 changes: 45 additions & 44 deletions src/main/java/io/json/compare/matcher/JsonArrayMatcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
import io.json.compare.JsonComparator;
import io.json.compare.util.MessageUtil;

import java.util.HashSet;
import java.util.Set;
import java.util.*;

class JsonArrayMatcher extends AbstractJsonMatcher {

Expand All @@ -18,79 +17,81 @@ class JsonArrayMatcher extends AbstractJsonMatcher {
}

@Override
public void match() throws MatcherException {
public List<String> match() {
List<String> diffs = new ArrayList<>();

for (int i = 0; i < expected.size(); i++) {
JsonNode expElement = expected.get(i);
if (isJsonPathNode(expElement)) {
new JsonMatcher(expElement, actual, comparator, compareModes).match();
diffs.addAll(new JsonMatcher(expElement, actual, comparator, compareModes).match());
} else {
matchWithActualJsonArray(i, expElement, actual);
diffs.addAll(matchWithJsonArray(i, expElement, actual));
}
}
if (compareModes.contains(CompareMode.JSON_ARRAY_NON_EXTENSIBLE) && expected.size() < actual.size()) {
throw new MatcherException("Actual JSON ARRAY has extra elements");
diffs.add("Actual JSON ARRAY has extra elements");
}
return diffs;
}

private void matchWithActualJsonArray(int expPosition, JsonNode expElement, JsonNode actual) throws MatcherException {
private List<String> matchWithJsonArray(int expPosition, JsonNode expElement, JsonNode actualArray) {
List<String> diffs = new ArrayList<>();
UseCase useCase = getUseCase(expElement);
boolean found = false;
actualElementsLoop:
for (int j = 0; j < actual.size(); j++) {

for (int j = 0; j < actualArray.size(); j++) {
if (matchedPositions.contains(j)) {
continue;
}
if (compareModes.contains(CompareMode.JSON_ARRAY_STRICT_ORDER) && j != expPosition) {
continue;
}
List<String> elementDiffs;
switch (useCase) {
case MATCH:
JsonNode actElement = actual.get(j);
try {
new JsonMatcher(expElement, actElement, comparator, compareModes).match();
} catch (MatcherException e) {
JsonNode actElement = actualArray.get(j);
elementDiffs = new JsonMatcher(expElement, actElement, comparator, compareModes).match();
if (elementDiffs.isEmpty()) {
matchedPositions.add(j);
return Collections.emptyList();
} else {
if (compareModes.contains(CompareMode.JSON_ARRAY_STRICT_ORDER)) {
throw new MatcherException(String
.format("JSON ARRAY elements differ at position %s:\n%s", expPosition + 1,
MessageUtil.cropL(JSONCompare.prettyPrint(expElement))));
diffs.add(String.format("JSON ARRAY elements differ at position %s:" +
System.lineSeparator() + "%s" + System.lineSeparator() +
"________diffs________" + System.lineSeparator() + "%s", expPosition + 1,
MessageUtil.cropL(JSONCompare.prettyPrint(expElement)), String.join(
System.lineSeparator() + "_____________________" + System.lineSeparator(), elementDiffs)));
return diffs;
}
continue actualElementsLoop;
}
found = true;
matchedPositions.add(j);
break actualElementsLoop;
break;
case MATCH_ANY:
matchedPositions.add(j);
return;
return Collections.emptyList();
case DO_NOT_MATCH:
actElement = actual.get(j);
if (!areOfSameType(expElement, actElement)) {
continue actualElementsLoop;
}
try {
new JsonMatcher(expElement, actElement, comparator, compareModes).match();
} catch (MatcherException e) {
found = true;
break actualElementsLoop;
actElement = actualArray.get(j);
if (areOfSameType(expElement, actElement)) {
elementDiffs = new JsonMatcher(expElement, actElement, comparator, compareModes).match();
if (!elementDiffs.isEmpty()) {
diffs.add("Expected element from position " + (expPosition + 1)
+ " was FOUND:" + System.lineSeparator() + MessageUtil.cropL(JSONCompare.prettyPrint(expElement)));
return diffs;
}
}
break;
case DO_NOT_MATCH_ANY:
throw new MatcherException("Expected element from position " + (expPosition + 1)
+ " was FOUND:\n" + MessageUtil.cropL(JSONCompare.prettyPrint(expElement)));
diffs.add(String.format("Expected condition %s from position %s was not met." +
" Actual JSON array has extra elements.",
expElement, expPosition + 1));
return diffs;
}
}
if (!found && useCase == UseCase.MATCH) {
throw new MatcherException("Expected element from position " + (expPosition + 1) + " was NOT FOUND:\n"
+ MessageUtil.cropL(JSONCompare.prettyPrint(expElement)));
}
if (found && useCase == UseCase.DO_NOT_MATCH) {
throw new MatcherException("Expected element from position " + (expPosition + 1)
+ " was FOUND:\n" + MessageUtil.cropL(JSONCompare.prettyPrint(expElement)));
}
if (useCase == UseCase.MATCH_ANY) {
throw new MatcherException("Expected condition of type MATCH_ANY from position " + (expPosition + 1)
+ " was NOT MET. Actual Json Array has no extra elements:\n"
if (useCase == UseCase.MATCH) {
diffs.add(System.lineSeparator() + "Expected element from position " + (expPosition + 1) + " was NOT FOUND:" + System.lineSeparator()
+ MessageUtil.cropL(JSONCompare.prettyPrint(expElement)));
} else if (useCase == UseCase.MATCH_ANY) {
diffs.add(String.format("Expected condition %s from position %s was not met." +
" Actual Json Array has no extra elements.", expElement, expPosition + 1));
}
return diffs;
}
}
20 changes: 12 additions & 8 deletions src/main/java/io/json/compare/matcher/JsonMatcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import io.json.compare.CompareMode;
import io.json.compare.JsonComparator;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;

public class JsonMatcher extends AbstractJsonMatcher {
Expand All @@ -13,20 +16,21 @@ public JsonMatcher(JsonNode expected, JsonNode actual, JsonComparator comparator
}

@Override
public void match() throws MatcherException {
public List<String> match() {
if (isJsonObject(expected) && isJsonObject(actual)) {
new JsonObjectMatcher(expected, actual, comparator, compareModes).match();
return new JsonObjectMatcher(expected, actual, comparator, compareModes).match();
} else if (isJsonArray(expected) && isJsonArray(actual)) {
new JsonArrayMatcher(expected, actual, comparator, compareModes).match();
return new JsonArrayMatcher(expected, actual, comparator, compareModes).match();
} else if (isValueNode(expected) && isValueNode(actual)) {
new JsonValueMatcher(expected, actual, comparator, compareModes).match();
return new JsonValueMatcher(expected, actual, comparator, compareModes).match();
} else if (isJsonPathNode(expected)) {
new JsonObjectMatcher(expected, actual, comparator, compareModes).match();
return new JsonObjectMatcher(expected, actual, comparator, compareModes).match();
} else if (isMissingNode(expected) && isMissingNode(actual)) {
//do nothing
return Collections.emptyList();
} else {
throw new MatcherException("Different JSON types: "
+ expected.getClass().getSimpleName() + " vs " + actual.getClass().getSimpleName());
List<String> diffs = new ArrayList<>();
diffs.add("Different JSON types: expected " + expected.getClass().getSimpleName() + " but got " + actual.getClass().getSimpleName());
return diffs;
}
}
}
Loading