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 spring-projectsgh-32791

Co-authored-by: Stéphane Nicoll <[email protected]>
  • Loading branch information
philwebb and snicoll committed May 23, 2024
1 parent 80faa94 commit da2758f
Show file tree
Hide file tree
Showing 7 changed files with 401 additions and 99 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 da2758f

Please sign in to comment.