Skip to content

Commit

Permalink
Introduce JSON comparison abstraction for tests
Browse files Browse the repository at this point in the history
This commit introduces a JsonComparator abstraction, with an
implementation using JSONAssert. Previously, JSONAssert was the only
choice.

Test APIs have been adapted to allow the new abstraction while relying
on JSONAssert still for high-level methods.

Closes gh-32791

Co-authored-by: Stéphane Nicoll <[email protected]>
  • Loading branch information
philwebb and snicoll committed May 23, 2024
1 parent 80faa94 commit d8dcd3a
Show file tree
Hide file tree
Showing 14 changed files with 539 additions and 118 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,6 @@
import org.assertj.core.api.AssertProvider;
import org.assertj.core.error.BasicErrorMessageFactory;
import org.assertj.core.internal.Failures;
import org.skyscreamer.jsonassert.JSONCompare;
import org.skyscreamer.jsonassert.JSONCompareMode;
import org.skyscreamer.jsonassert.JSONCompareResult;
import org.skyscreamer.jsonassert.comparator.JSONComparator;

import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.ClassPathResource;
Expand All @@ -41,7 +37,6 @@
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.function.ThrowingBiFunction;

/**
* Base AssertJ {@linkplain org.assertj.core.api.Assert assertions} that can be
Expand All @@ -51,8 +46,8 @@
* extracting a part of the document for further {@linkplain JsonPathValueAssert
* assertions} on the value.
*
* <p>Also supports comparing the JSON document against a target, using
* {@linkplain JSONCompare JSON Assert}. Resources that are loaded from
* <p>Also supports comparing the JSON document against a target, using a
* {@linkplain JsonComparator JSON Comparator}. Resources that are loaded from
* the classpath can be relative if a {@linkplain #withResourceLoadClass(Class)
* class} is provided. By default, {@code UTF-8} is used to load resources,
* but this can be overridden using {@link #withCharset(Charset)}.
Expand Down Expand Up @@ -154,9 +149,9 @@ public SELF doesNotHavePath(String path) {
* the expected JSON
* @param compareMode the compare mode used when checking
*/
public SELF isEqualTo(@Nullable CharSequence expected, JSONCompareMode compareMode) {
public SELF isEqualTo(@Nullable CharSequence expected, JsonCompareMode compareMode) {
String expectedJson = this.jsonLoader.getJson(expected);
return assertNotFailed(compare(expectedJson, compareMode));
return assertIsMatch(compare(expectedJson, compareMode));
}

/**
Expand All @@ -171,9 +166,9 @@ public SELF isEqualTo(@Nullable CharSequence expected, JSONCompareMode compareMo
* @param expected a resource containing the expected JSON
* @param compareMode the compare mode used when checking
*/
public SELF isEqualTo(Resource expected, JSONCompareMode compareMode) {
public SELF isEqualTo(Resource expected, JsonCompareMode compareMode) {
String expectedJson = this.jsonLoader.getJson(expected);
return assertNotFailed(compare(expectedJson, compareMode));
return assertIsMatch(compare(expectedJson, compareMode));
}

/**
Expand All @@ -184,9 +179,9 @@ public SELF isEqualTo(Resource expected, JSONCompareMode compareMode) {
* the expected JSON
* @param comparator the comparator used when checking
*/
public SELF isEqualTo(@Nullable CharSequence expected, JSONComparator comparator) {
public SELF isEqualTo(@Nullable CharSequence expected, JsonComparator comparator) {
String expectedJson = this.jsonLoader.getJson(expected);
return assertNotFailed(compare(expectedJson, comparator));
return assertIsMatch(compare(expectedJson, comparator));
}

/**
Expand All @@ -201,25 +196,25 @@ public SELF isEqualTo(@Nullable CharSequence expected, JSONComparator comparator
* @param expected a resource containing the expected JSON
* @param comparator the comparator used when checking
*/
public SELF isEqualTo(Resource expected, JSONComparator comparator) {
public SELF isEqualTo(Resource expected, JsonComparator comparator) {
String expectedJson = this.jsonLoader.getJson(expected);
return assertNotFailed(compare(expectedJson, comparator));
return assertIsMatch(compare(expectedJson, comparator));
}

/**
* Verify that the actual value is {@link JSONCompareMode#LENIENT leniently}
* Verify that the actual value is {@link JsonCompareMode#LENIENT leniently}
* equal to the given JSON. The {@code expected} value can contain the JSON
* itself or, if it ends with {@code .json}, the name of a resource to be
* loaded from the classpath.
* @param expected the expected JSON or the name of a resource containing
* the expected JSON
*/
public SELF isLenientlyEqualTo(@Nullable CharSequence expected) {
return isEqualTo(expected, JSONCompareMode.LENIENT);
return isEqualTo(expected, JsonCompareMode.LENIENT);
}

/**
* Verify that the actual value is {@link JSONCompareMode#LENIENT leniently}
* Verify that the actual value is {@link JsonCompareMode#LENIENT leniently}
* equal to the given JSON {@link Resource}.
* <p>The resource abstraction allows to provide several input types:
* <ul>
Expand All @@ -231,23 +226,23 @@ public SELF isLenientlyEqualTo(@Nullable CharSequence expected) {
* @param expected a resource containing the expected JSON
*/
public SELF isLenientlyEqualTo(Resource expected) {
return isEqualTo(expected, JSONCompareMode.LENIENT);
return isEqualTo(expected, JsonCompareMode.LENIENT);
}

/**
* Verify that the actual value is {@link JSONCompareMode#STRICT strictly}
* Verify that the actual value is {@link JsonCompareMode#STRICT strictly}
* equal to the given JSON. The {@code expected} value can contain the JSON
* itself or, if it ends with {@code .json}, the name of a resource to be
* loaded from the classpath.
* @param expected the expected JSON or the name of a resource containing
* the expected JSON
*/
public SELF isStrictlyEqualTo(@Nullable CharSequence expected) {
return isEqualTo(expected, JSONCompareMode.STRICT);
return isEqualTo(expected, JsonCompareMode.STRICT);
}

/**
* Verify that the actual value is {@link JSONCompareMode#STRICT strictly}
* Verify that the actual value is {@link JsonCompareMode#STRICT strictly}
* equal to the given JSON {@link Resource}.
* <p>The resource abstraction allows to provide several input types:
* <ul>
Expand All @@ -259,7 +254,7 @@ public SELF isStrictlyEqualTo(@Nullable CharSequence expected) {
* @param expected a resource containing the expected JSON
*/
public SELF isStrictlyEqualTo(Resource expected) {
return isEqualTo(expected, JSONCompareMode.STRICT);
return isEqualTo(expected, JsonCompareMode.STRICT);
}

/**
Expand All @@ -270,9 +265,9 @@ public SELF isStrictlyEqualTo(Resource expected) {
* the expected JSON
* @param compareMode the compare mode used when checking
*/
public SELF isNotEqualTo(@Nullable CharSequence expected, JSONCompareMode compareMode) {
public SELF isNotEqualTo(@Nullable CharSequence expected, JsonCompareMode compareMode) {
String expectedJson = this.jsonLoader.getJson(expected);
return assertNotPassed(compare(expectedJson, compareMode));
return assertIsMismatch(compare(expectedJson, compareMode));
}

/**
Expand All @@ -287,9 +282,9 @@ public SELF isNotEqualTo(@Nullable CharSequence expected, JSONCompareMode compar
* @param expected a resource containing the expected JSON
* @param compareMode the compare mode used when checking
*/
public SELF isNotEqualTo(Resource expected, JSONCompareMode compareMode) {
public SELF isNotEqualTo(Resource expected, JsonCompareMode compareMode) {
String expectedJson = this.jsonLoader.getJson(expected);
return assertNotPassed(compare(expectedJson, compareMode));
return assertIsMismatch(compare(expectedJson, compareMode));
}

/**
Expand All @@ -300,9 +295,9 @@ public SELF isNotEqualTo(Resource expected, JSONCompareMode compareMode) {
* the expected JSON
* @param comparator the comparator used when checking
*/
public SELF isNotEqualTo(@Nullable CharSequence expected, JSONComparator comparator) {
public SELF isNotEqualTo(@Nullable CharSequence expected, JsonComparator comparator) {
String expectedJson = this.jsonLoader.getJson(expected);
return assertNotPassed(compare(expectedJson, comparator));
return assertIsMismatch(compare(expectedJson, comparator));
}

/**
Expand All @@ -317,25 +312,25 @@ public SELF isNotEqualTo(@Nullable CharSequence expected, JSONComparator compara
* @param expected a resource containing the expected JSON
* @param comparator the comparator used when checking
*/
public SELF isNotEqualTo(Resource expected, JSONComparator comparator) {
public SELF isNotEqualTo(Resource expected, JsonComparator comparator) {
String expectedJson = this.jsonLoader.getJson(expected);
return assertNotPassed(compare(expectedJson, comparator));
return assertIsMismatch(compare(expectedJson, comparator));
}

/**
* Verify that the actual value is not {@link JSONCompareMode#LENIENT
* Verify that the actual value is not {@link JsonCompareMode#LENIENT
* leniently} equal to the given JSON. The {@code expected} value can
* contain the JSON itself or, if it ends with {@code .json}, the name of a
* resource to be loaded from the classpath.
* @param expected the expected JSON or the name of a resource containing
* the expected JSON
*/
public SELF isNotLenientlyEqualTo(@Nullable CharSequence expected) {
return isNotEqualTo(expected, JSONCompareMode.LENIENT);
return isNotEqualTo(expected, JsonCompareMode.LENIENT);
}

/**
* Verify that the actual value is not {@link JSONCompareMode#LENIENT
* Verify that the actual value is not {@link JsonCompareMode#LENIENT
* leniently} equal to the given JSON {@link Resource}.
* <p>The resource abstraction allows to provide several input types:
* <ul>
Expand All @@ -347,23 +342,23 @@ public SELF isNotLenientlyEqualTo(@Nullable CharSequence expected) {
* @param expected a resource containing the expected JSON
*/
public SELF isNotLenientlyEqualTo(Resource expected) {
return isNotEqualTo(expected, JSONCompareMode.LENIENT);
return isNotEqualTo(expected, JsonCompareMode.LENIENT);
}

/**
* Verify that the actual value is not {@link JSONCompareMode#STRICT
* Verify that the actual value is not {@link JsonCompareMode#STRICT
* strictly} equal to the given JSON. The {@code expected} value can
* contain the JSON itself or, if it ends with {@code .json}, the name of a
* resource to be loaded from the classpath.
* @param expected the expected JSON or the name of a resource containing
* the expected JSON
*/
public SELF isNotStrictlyEqualTo(@Nullable CharSequence expected) {
return isNotEqualTo(expected, JSONCompareMode.STRICT);
return isNotEqualTo(expected, JsonCompareMode.STRICT);
}

/**
* Verify that the actual value is not {@link JSONCompareMode#STRICT
* Verify that the actual value is not {@link JsonCompareMode#STRICT
* strictly} equal to the given JSON {@link Resource}.
* <p>The resource abstraction allows to provide several input types:
* <ul>
Expand All @@ -375,7 +370,7 @@ public SELF isNotStrictlyEqualTo(@Nullable CharSequence expected) {
* @param expected a resource containing the expected JSON
*/
public SELF isNotStrictlyEqualTo(Resource expected) {
return isNotEqualTo(expected, JSONCompareMode.STRICT);
return isNotEqualTo(expected, JsonCompareMode.STRICT);
}

/**
Expand Down Expand Up @@ -405,54 +400,25 @@ public SELF withCharset(@Nullable Charset charset) {
}


private JSONCompareResult compare(@Nullable CharSequence expectedJson, JSONCompareMode compareMode) {
return compare(this.actual, expectedJson, (actualJsonString, expectedJsonString) ->
JSONCompare.compareJSON(expectedJsonString, actualJsonString, compareMode));
private JsonComparison compare(@Nullable CharSequence expectedJson, JsonCompareMode compareMode) {
return compare(expectedJson, JsonAssert.comparator(compareMode));
}

private JSONCompareResult compare(@Nullable CharSequence expectedJson, JSONComparator comparator) {
return compare(this.actual, expectedJson, (actualJsonString, expectedJsonString) ->
JSONCompare.compareJSON(expectedJsonString, actualJsonString, comparator));
private JsonComparison compare(@Nullable CharSequence expectedJson, JsonComparator comparator) {
return comparator.compare((expectedJson != null) ? expectedJson.toString() : null, this.actual);
}

private JSONCompareResult compare(@Nullable CharSequence actualJson, @Nullable CharSequence expectedJson,
ThrowingBiFunction<String, String, JSONCompareResult> comparator) {

if (actualJson == null) {
return compareForNull(expectedJson);
}
if (expectedJson == null) {
return compareForNull(actualJson.toString());
}
try {
return comparator.applyWithException(actualJson.toString(), expectedJson.toString());
}
catch (Exception ex) {
if (ex instanceof RuntimeException runtimeException) {
throw runtimeException;
}
throw new IllegalStateException(ex);
}
private SELF assertIsMatch(JsonComparison result) {
return assertComparison(result, JsonComparison.Result.MATCH);
}

private JSONCompareResult compareForNull(@Nullable CharSequence expectedJson) {
JSONCompareResult result = new JSONCompareResult();
if (expectedJson != null) {
result.fail("Expected null JSON");
}
return result;
}

private SELF assertNotFailed(JSONCompareResult result) {
if (result.failed()) {
failWithMessage("JSON comparison failure: %s", result.getMessage());
}
return this.myself;
private SELF assertIsMismatch(JsonComparison result) {
return assertComparison(result, JsonComparison.Result.MISMATCH);
}

private SELF assertNotPassed(JSONCompareResult result) {
if (result.passed()) {
failWithMessage("JSON comparison failure: %s", result.getMessage());
private SELF assertComparison(JsonComparison jsonComparison, JsonComparison.Result requiredResult) {
if (jsonComparison.getResult() != requiredResult) {
failWithMessage("JSON comparison failure: %s", jsonComparison.getMessage());
}
return this.myself;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.test.json;

import org.skyscreamer.jsonassert.JSONCompare;
import org.skyscreamer.jsonassert.JSONCompareMode;
import org.skyscreamer.jsonassert.JSONCompareResult;
import org.skyscreamer.jsonassert.comparator.JSONComparator;

import org.springframework.lang.Nullable;
import org.springframework.util.function.ThrowingBiFunction;

/**
* Useful methods that can be used with {@code org.skyscreamer.jsonassert}.
*
* @author Phillip Webb
* @since 6.2
*/
public abstract class JsonAssert {

/**
* Create a {@link JsonComparator} from the given {@link JsonCompareMode}.
* @param compareMode the mode to use
* @return a new {@link JsonComparator} instance
* @see JSONCompareMode#STRICT
* @see JSONCompareMode#LENIENT
*/
public static JsonComparator comparator(JsonCompareMode compareMode) {
return comparator(toJSONCompareMode(compareMode));
}

/**
* Create a new {@link JsonComparator} from the given JSONAssert
* {@link JSONComparator}.
* @param comparator the JSON Assert {@link JSONComparator}
* @return a new {@link JsonComparator} instance
*/
public static JsonComparator comparator(JSONComparator comparator) {
return comparator((expectedJson, actualJson) -> JSONCompare
.compareJSON(expectedJson, actualJson, comparator));
}

/**
* Create a new {@link JsonComparator} from the given JSONAssert
* {@link JSONCompareMode}.
* @param mode the JSON Assert {@link JSONCompareMode}
* @return a new {@link JsonComparator} instance
*/
public static JsonComparator comparator(JSONCompareMode mode) {
return comparator((expectedJson, actualJson) -> JSONCompare
.compareJSON(expectedJson, actualJson, mode));
}

private static JsonComparator comparator(ThrowingBiFunction<String, String, JSONCompareResult> compareFunction) {
return (expectedJson, actualJson) -> compare(expectedJson, actualJson, compareFunction);
}

private static JsonComparison compare(@Nullable String expectedJson, @Nullable String actualJson,
ThrowingBiFunction<String, String, JSONCompareResult> compareFunction) {

if (actualJson == null) {
return (expectedJson != null)
? JsonComparison.mismatch("Expected null JSON")
: JsonComparison.match();
}
if (expectedJson == null) {
return JsonComparison.mismatch("Expected non-null JSON");
}
JSONCompareResult result = compareFunction.throwing(IllegalStateException::new).apply(expectedJson, actualJson);
return (!result.passed())
? JsonComparison.mismatch(result.getMessage())
: JsonComparison.match();
}

private static JSONCompareMode toJSONCompareMode(JsonCompareMode compareMode) {
return (compareMode != JsonCompareMode.LENIENT ? JSONCompareMode.STRICT : JSONCompareMode.LENIENT);
}

}
Loading

0 comments on commit d8dcd3a

Please sign in to comment.