Merge pull request #2198 from ebean-orm/sebastian-mrozek-feature/json-assert-compare-arrays

Feature/json assert compare arrays
This commit is contained in:
Rob Bygrave
2021-03-16 22:06:28 +13:00
committed by GitHub
14 changed files with 400 additions and 46 deletions
@@ -0,0 +1,39 @@
package io.ebean.test;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
final class CompareResult {
private final boolean applicable;
private final List<String> errors;
static final CompareResult NO_ERRORS = new CompareResult(true, Collections.emptyList());
static final CompareResult NOT_APPLICABLE = new CompareResult(false, Collections.emptyList());
static CompareResult error(String error) {
return new CompareResult(true, Collections.singletonList(error));
}
static CompareResult errors(List<String> errors) {
return new CompareResult(true, errors);
}
private CompareResult(boolean applicable, List<String> errors) {
this.applicable = applicable;
this.errors = new ArrayList<>(errors);
}
boolean isApplicable() {
return applicable;
}
boolean hasErrors() {
return !errors.isEmpty();
}
List<String> getErrors() {
return errors;
}
}
@@ -3,10 +3,7 @@ package io.ebean.test;
import com.fasterxml.jackson.databind.JsonNode;
import org.assertj.core.api.Assertions;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.Stack;
import java.util.*;
/**
* Perform traversal of JsonNodes comparing against an expected JsonNode that
@@ -16,15 +13,15 @@ import java.util.Stack;
class JsonAssertContains {
private final Stack<String> path = new Stack<>();
private final LinkedList<String> errors = new LinkedList<>();
static void assertContains(JsonNode actualJsonNode, JsonNode expectedJsonNode) {
new JsonAssertContains().contains(actualJsonNode, expectedJsonNode);
}
private void contains(JsonNode actualJsonNode, JsonNode expectedJsonNode) {
checkRecursive(null, actualJsonNode, expectedJsonNode);
if (!errors.isEmpty()) {
CompareResult result = checkRecursive(null, actualJsonNode, expectedJsonNode);
if (result.hasErrors()) {
List<String> errors = result.getErrors();
String errorsString = String.join("\n", errors);
errorsString += "\nExpected JSON fields: " + expectedJsonNode;
errorsString += "\nActual JSON: " + actualJsonNode;
@@ -32,55 +29,72 @@ class JsonAssertContains {
}
}
private void checkRecursive(String name, JsonNode actualJsonNode, JsonNode expectedJsonNode) {
private CompareResult checkRecursive(String name, JsonNode actualJsonNode, JsonNode expectedJsonNode) {
if (name != null) {
path.push(name);
}
if (checkNull(actualJsonNode, expectedJsonNode)) {
if (checkType(actualJsonNode, expectedJsonNode)) {
if (checkArray(actualJsonNode, expectedJsonNode)) {
if (checkObject(actualJsonNode, expectedJsonNode)) {
checkValue(actualJsonNode, expectedJsonNode);
}
}
}
CompareResult result = checkNull(actualJsonNode, expectedJsonNode);
if (result.isApplicable()) {
return pop(name, result);
}
result = checkType(actualJsonNode, expectedJsonNode);
if (result.isApplicable()) {
return pop(name, result);
}
result = checkArray(actualJsonNode, expectedJsonNode);
if (result.isApplicable()) {
return pop(name, result);
}
result = checkObject(actualJsonNode, expectedJsonNode);
if (result.isApplicable()) {
return pop(name, result);
}
result = checkValue(actualJsonNode, expectedJsonNode);
if (result.isApplicable()) {
return pop(name, result);
}
return CompareResult.NOT_APPLICABLE;
}
private CompareResult pop(String name, CompareResult result) {
if (name != null) {
path.pop();
}
return result;
}
private boolean checkNull(JsonNode actualJsonNode, JsonNode expectedJsonNode) {
private CompareResult checkNull(JsonNode actualJsonNode, JsonNode expectedJsonNode) {
if (actualJsonNode == null) {
errors.add(String.format("Expected field '%s' to be '%s' but was null", path(), expectedJsonNode));
return false;
return CompareResult.error(String.format("Expected field '%s' to be '%s' but was null", path(), expectedJsonNode));
}
return true;
return CompareResult.NOT_APPLICABLE;
}
private boolean checkType(JsonNode actualJsonNode, JsonNode expectedJsonNode) {
private CompareResult checkType(JsonNode actualJsonNode, JsonNode expectedJsonNode) {
if (!expectedJsonNode.getNodeType().equals(actualJsonNode.getNodeType())) {
errors.add(String.format("Expected field '%s' to be of type '%s' but was '%s'", path(), expectedJsonNode.getNodeType(), actualJsonNode.getNodeType()));
return false;
return CompareResult.error(String.format("Expected field '%s' to be of type '%s' but was '%s'", path(), expectedJsonNode.getNodeType(), actualJsonNode.getNodeType()));
}
return true;
return CompareResult.NOT_APPLICABLE;
}
private boolean checkArray(JsonNode actualJsonNode, JsonNode expectedJsonNode) {
private CompareResult checkArray(JsonNode actualJsonNode, JsonNode expectedJsonNode) {
if (!expectedJsonNode.isArray()) {
return true;
return CompareResult.NOT_APPLICABLE;
}
for (int i = 0; i < expectedJsonNode.size(); i++) {
checkRecursive("[" + i + "]", actualJsonNode.get(i), expectedJsonNode.get(i));
}
// do not continue (object or scalar type check)
return false;
return new MatchArrayElements(actualJsonNode, expectedJsonNode).match();
}
private boolean checkObject(JsonNode actualJsonNode, JsonNode expectedJsonNode) {
private CompareResult checkObject(JsonNode actualJsonNode, JsonNode expectedJsonNode) {
if (!expectedJsonNode.isObject()) {
return true;
return CompareResult.NOT_APPLICABLE;
}
List<String> errors = new LinkedList<>();
Iterator<Map.Entry<String, JsonNode>> expectedFields = expectedJsonNode.fields();
while (expectedFields.hasNext()) {
Map.Entry<String, JsonNode> expectedField = expectedFields.next();
@@ -89,17 +103,18 @@ class JsonAssertContains {
if (actualNode == null) {
errors.add(String.format("Expected field '%s' to be present", path(expectedKey)));
} else {
checkRecursive(expectedKey, actualNode, expectedField.getValue());
CompareResult result = checkRecursive(expectedKey, actualNode, expectedField.getValue());
errors.addAll(result.getErrors());
}
}
// do not continue (scalar type check)
return false;
return CompareResult.errors(errors);
}
private void checkValue(JsonNode actualJsonNode, JsonNode expectedJsonNode) {
private CompareResult checkValue(JsonNode actualJsonNode, JsonNode expectedJsonNode) {
if (!expectedJsonNode.equals(actualJsonNode)) {
errors.add(String.format("Expected field '%s' to be equal to '%s' but was '%s'", path(), expectedJsonNode, actualJsonNode));
return CompareResult.error(String.format("Expected field '%s' to be equal to '%s' but was '%s'", path(), expectedJsonNode, actualJsonNode));
}
return CompareResult.NO_ERRORS;
}
String path(String expectedKey) {
@@ -115,4 +130,64 @@ class JsonAssertContains {
}
return String.join(".", path).replace(".[", "[");
}
/**
* Match two arrays allowing elements to be in a different order.
*/
private class MatchArrayElements {
private final JsonNode actualJsonNode;
private final JsonNode expectedJsonNode;
private final Map<Integer, JsonNode> expectedMap = new LinkedHashMap<>();
private final Map<Integer, JsonNode> actualMap = new LinkedHashMap<>();
MatchArrayElements(JsonNode actualJsonNode, JsonNode expectedJsonNode) {
this.actualJsonNode = actualJsonNode;
this.expectedJsonNode = expectedJsonNode;
for (int e = 0; e < expectedJsonNode.size(); e++) {
expectedMap.put(e, expectedJsonNode.get(e));
}
for (int a = 0; a < actualJsonNode.size(); a++) {
actualMap.put(a, actualJsonNode.get(a));
}
}
CompareResult match() {
Iterator<Map.Entry<Integer, JsonNode>> iterator = expectedMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Integer, JsonNode> expectedEntry = iterator.next();
int matchPos = findFirstMatch(expectedEntry.getKey(), expectedEntry.getValue());
if (matchPos > -1) {
iterator.remove();
}
}
List<String> errors = new ArrayList<>();
if (actualJsonNode.size() != expectedJsonNode.size()) {
errors.add(String.format("Unmatched array size for '%s', expected %d but got %d elements", path(), expectedJsonNode.size(), actualJsonNode.size()));
}
// unmatched expected -> actual
for (Map.Entry<Integer, JsonNode> entry : expectedMap.entrySet()) {
errors.add(String.format("Expected array element '%s[%d]' was not matched to an element in the actual array - element: %s", path(), entry.getKey(), entry.getValue()));
}
// unmatched actual -> expected
for (Map.Entry<Integer, JsonNode> entry : actualMap.entrySet()) {
errors.add(String.format("Actual array element '%s[%d]' was not matched to an element in the expected array - element: %s", path(), entry.getKey(), entry.getValue()));
}
return CompareResult.errors(errors);
}
private int findFirstMatch(int e, JsonNode expectedNode) {
Iterator<Map.Entry<Integer, JsonNode>> iterator = actualMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Integer, JsonNode> entry = iterator.next();
CompareResult result = checkRecursive("[" + e + "]", entry.getValue(), expectedNode);
if (result.isApplicable() && !result.hasErrors()) {
iterator.remove();
return entry.getKey();
}
}
return -1;
}
}
}
@@ -1,10 +1,12 @@
package io.ebean.test;
import com.fasterxml.jackson.databind.JsonNode;
import org.assertj.core.api.Assertions;
import org.junit.Test;
import java.util.stream.Stream;
import static io.ebean.test.Json.readNode;
import static io.ebean.test.Json.readNodeFromResource;
import static org.assertj.core.api.Assertions.assertThat;
@@ -33,18 +35,21 @@ public class JsonAssertContainsTest {
JsonAssertContains.assertContains(original, expected);
} catch (AssertionError e) {
String exceptionMessage = e.getMessage();
System.out.println(exceptionMessage);
Stream.of("Expected field 'someString1' to be equal to '\"aaaa\"' but was '\"string1\"",
"Expected field 'someValue1' to be equal to '99' but was '1'",
"Expected field 'someArray1[0]' to be of type 'STRING' but was 'NUMBER",
"Expected field 'someArray2[0].value1' to be of type 'ARRAY' but was 'NUMBER'",
"Expected field 'someArray2[0].value2' to be of type 'OBJECT' but was 'STRING'",
"Expected field 'someArray2[0].array1[0]' to be '\"1\"' but was null",
"Expected field 'someArray2[0].object1.val5' to be present",
"Expected field 'someArray2[0].object1.val6' to be present",
"Expected field 'someArray2[0].object2' to be of type 'NULL' but was 'OBJECT'",
"Expected field 'someArray2[0].objectNull' to be of type 'OBJECT' but was 'NULL'")
"Expected field 'someObject1.value1' to be of type 'ARRAY' but was 'NUMBER'",
"Expected field 'someObject1.value2' to be of type 'OBJECT' but was 'STRING'",
"Expected array element 'someObject1.array1[0]' was not matched to an element in the actual array - element: 101",
"Actual array element 'someObject1.array1[0]' was not matched to an element in the expected array - element: 99",
"Expected field 'someObject1.object1.val5' to be present",
"Expected field 'someObject1.object1.val6' to be present",
"Expected field 'someObject1.object2' to be of type 'NULL' but was 'OBJECT'",
"Expected field 'someObject1.objectNull' to be of type 'OBJECT' but was 'NULL'")
.forEach(assertionError -> assertThat(exceptionMessage).contains(assertionError));
return;
}
Assertions.fail("Expected an exception to be thrown");
}
@@ -59,7 +64,9 @@ public class JsonAssertContainsTest {
Stream.of("Expected field 'someNull' to be of type 'NULL' but was 'STRING'",
"Expected field 'extra' to be present")
.forEach(assertionError -> assertThat(exceptionMessage).contains(assertionError));
return;
}
Assertions.fail("Expected an exception to be thrown");
}
@Test
@@ -72,7 +79,9 @@ public class JsonAssertContainsTest {
String exceptionMessage = e.getMessage();
Stream.of("Expected field 'some' to be of type 'NUMBER' but was 'STRING'")
.forEach(assertionError -> assertThat(exceptionMessage).contains(assertionError));
return;
}
Assertions.fail("Expected an exception to be thrown");
}
@Test
@@ -83,4 +92,103 @@ public class JsonAssertContainsTest {
assertThat(contains.path("a")).isEqualTo("a");
assertThat(contains.path("b")).isEqualTo("b");
}
@Test
public void assertContainsNumbersArrayShuffled() {
JsonNode array = readNode("[2, 54, 13, 10]");
JsonNode arrayShuffled = readNode("[2, 13, 10, 54]");
JsonAssertContains.assertContains(arrayShuffled, array);
}
@Test
public void assertContainsObjectsArrayShuffled() {
JsonNode array = readNodeFromResource("/contains/array-objects.json");
JsonNode arrayShuffled = readNodeFromResource("/contains/array-objects-shuffled.json");
JsonAssertContains.assertContains(arrayShuffled, array);
}
@Test
public void assertArrayElementsNotFound() {
JsonNode original = readNodeFromResource("/contains/array-multi-match.json");
JsonNode actual = readNodeFromResource("/contains/array-multi-match-duplicate-props.json");
try {
JsonAssertContains.assertContains(actual, original);
} catch (AssertionError e) {
String exceptionMessage = e.getMessage();
Stream.of("Unmatched array size for '', expected 6 but got 4 elements",
"Expected array element '[2]' was not matched to an element in the actual array - element: {\"c\":3}",
"Expected array element '[4]' was not matched to an element in the actual array - element: {\"e\":5}",
"Expected array element '[5]' was not matched to an element in the actual array - element: {\"f\":6}",
"Actual array element '[3]' was not matched to an element in the expected array - element: {\"b\":2}")
.forEach(assertionError -> assertThat(exceptionMessage).contains(assertionError));
return;
}
Assertions.fail("Expected an exception to be thrown");
}
@Test
public void assertNestedArray_ordering() {
JsonNode original = readNodeFromResource("/contains/nested-array.json");
JsonNode actual = readNodeFromResource("/contains/nested-array-ordering.json");
JsonAssertContains.assertContains(actual, original);
}
@Test
public void assertNestedArray_both() {
try {
JsonNode original = readNodeFromResource("/contains/nested-array.json");
JsonNode actual = readNodeFromResource("/contains/nested-array-both.json");
JsonAssertContains.assertContains(actual, original);
} catch (AssertionError e) {
String exceptionMessage = e.getMessage();
Stream.of(
"Expected array element 'someArray1[1]' was not matched to an element in the actual array - element: {\"name\":\"b\"}",
"Actual array element 'someArray1[0]' was not matched to an element in the expected array - element: {\"name\":\"z\"}")
.forEach(assertionError -> assertThat(exceptionMessage).contains(assertionError));
return;
}
Assertions.fail("Expected an exception to be thrown");
}
@Test
public void assertNestedArray_extraExpected() {
try {
JsonNode original = readNodeFromResource("/contains/nested-array.json");
JsonNode actual = readNodeFromResource("/contains/nested-array-extraExpected.json");
JsonAssertContains.assertContains(actual, original);
} catch (AssertionError e) {
String exceptionMessage = e.getMessage();
Stream.of(
"Unmatched array size for 'someArray1', expected 3 but got 4 elements",
"Actual array element 'someArray1[2]' was not matched to an element in the expected array - element: {\"name\":\"d\"}")
.forEach(assertionError -> assertThat(exceptionMessage).contains(assertionError));
return;
}
Assertions.fail("Expected an exception to be thrown");
}
@Test
public void assertNestedArray_missExpected() {
try {
JsonNode original = readNodeFromResource("/contains/nested-array.json");
JsonNode actual = readNodeFromResource("/contains/nested-array-missExpected.json");
JsonAssertContains.assertContains(actual, original);
} catch (AssertionError e) {
String exceptionMessage = e.getMessage();
Stream.of(
"Unmatched array size for 'someArray1', expected 3 but got 2 elements",
"Expected array element 'someArray1[1]' was not matched to an element in the actual array - element: {\"name\":\"b\"}")
.forEach(assertionError -> assertThat(exceptionMessage).contains(assertionError));
return;
}
Assertions.fail("Expected an exception to be thrown");
}
}
@@ -0,0 +1,18 @@
[
{
"b": 2,
"c": 3
},
{
"d": 4,
"e": 5
},
{
"a": 1,
"b": 2,
"c": 3
},
{
"b": 2
}
]
@@ -0,0 +1,20 @@
[
{
"a": 1
},
{
"b": 2
},
{
"c": 3
},
{
"d": 4
},
{
"e": 5
},
{
"f": 6
}
]
@@ -0,0 +1,14 @@
[
{
"id": "tyu",
"c": 3
},
{
"id": "zxy",
"a": 1
},
{
"id": "123",
"b": 2
}
]
@@ -0,0 +1,11 @@
[
{
"a": 1
},
{
"b": 2
},
{
"c": 3
}
]
@@ -0,0 +1,8 @@
{
"someString1": "string1",
"someArray1": [
{"name":"z"},
{"name":"c"},
{"name":"a"}
]
}
@@ -0,0 +1,9 @@
{
"someString1": "string1",
"someArray1": [
{"name":"c"},
{"name":"a"},
{"name":"d"},
{"name":"b"}
]
}
@@ -0,0 +1,7 @@
{
"someString1": "string1",
"someArray1": [
{"name":"c"},
{"name":"a"}
]
}
@@ -0,0 +1,8 @@
{
"someString1": "string1",
"someArray1": [
{"name":"c"},
{"name":"a"},
{"name":"b"}
]
}
@@ -0,0 +1,8 @@
{
"someString1": "string1",
"someArray1": [
{"name":"a"},
{"name":"b"},
{"name":"c"}
]
}
@@ -3,6 +3,22 @@
"someString2": "string2",
"someValue1": 99,
"someValue2": 2,
"someObject1": {
"value1": [],
"value2": {
},
"array1": [
101
],
"object1": {
"val5": "val5",
"val6": "val6"
},
"object2": null,
"objectNull": {
}
},
"someArray1": [
"a"
],
@@ -3,6 +3,19 @@
"someString2": "string2",
"someValue1": 1,
"someValue2": 2,
"someObject1": {
"value1": 42,
"value2": "string3",
"array1": [
99
],
"object1": {
},
"object2": {
},
"objectNull": null
},
"someArray1": [
1,
2,