Skip to content

Commit d8dcd3a

Browse files
philwebbsnicoll
andcommitted
Introduce JSON comparison abstraction for tests
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]>
1 parent 80faa94 commit d8dcd3a

File tree

14 files changed

+539
-118
lines changed

14 files changed

+539
-118
lines changed

spring-test/src/main/java/org/springframework/test/json/AbstractJsonContentAssert.java

+45-79
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,6 @@
2828
import org.assertj.core.api.AssertProvider;
2929
import org.assertj.core.error.BasicErrorMessageFactory;
3030
import org.assertj.core.internal.Failures;
31-
import org.skyscreamer.jsonassert.JSONCompare;
32-
import org.skyscreamer.jsonassert.JSONCompareMode;
33-
import org.skyscreamer.jsonassert.JSONCompareResult;
34-
import org.skyscreamer.jsonassert.comparator.JSONComparator;
3531

3632
import org.springframework.core.io.ByteArrayResource;
3733
import org.springframework.core.io.ClassPathResource;
@@ -41,7 +37,6 @@
4137
import org.springframework.http.converter.GenericHttpMessageConverter;
4238
import org.springframework.lang.Nullable;
4339
import org.springframework.util.Assert;
44-
import org.springframework.util.function.ThrowingBiFunction;
4540

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

162157
/**
@@ -171,9 +166,9 @@ public SELF isEqualTo(@Nullable CharSequence expected, JSONCompareMode compareMo
171166
* @param expected a resource containing the expected JSON
172167
* @param compareMode the compare mode used when checking
173168
*/
174-
public SELF isEqualTo(Resource expected, JSONCompareMode compareMode) {
169+
public SELF isEqualTo(Resource expected, JsonCompareMode compareMode) {
175170
String expectedJson = this.jsonLoader.getJson(expected);
176-
return assertNotFailed(compare(expectedJson, compareMode));
171+
return assertIsMatch(compare(expectedJson, compareMode));
177172
}
178173

179174
/**
@@ -184,9 +179,9 @@ public SELF isEqualTo(Resource expected, JSONCompareMode compareMode) {
184179
* the expected JSON
185180
* @param comparator the comparator used when checking
186181
*/
187-
public SELF isEqualTo(@Nullable CharSequence expected, JSONComparator comparator) {
182+
public SELF isEqualTo(@Nullable CharSequence expected, JsonComparator comparator) {
188183
String expectedJson = this.jsonLoader.getJson(expected);
189-
return assertNotFailed(compare(expectedJson, comparator));
184+
return assertIsMatch(compare(expectedJson, comparator));
190185
}
191186

192187
/**
@@ -201,25 +196,25 @@ public SELF isEqualTo(@Nullable CharSequence expected, JSONComparator comparator
201196
* @param expected a resource containing the expected JSON
202197
* @param comparator the comparator used when checking
203198
*/
204-
public SELF isEqualTo(Resource expected, JSONComparator comparator) {
199+
public SELF isEqualTo(Resource expected, JsonComparator comparator) {
205200
String expectedJson = this.jsonLoader.getJson(expected);
206-
return assertNotFailed(compare(expectedJson, comparator));
201+
return assertIsMatch(compare(expectedJson, comparator));
207202
}
208203

209204
/**
210-
* Verify that the actual value is {@link JSONCompareMode#LENIENT leniently}
205+
* Verify that the actual value is {@link JsonCompareMode#LENIENT leniently}
211206
* equal to the given JSON. The {@code expected} value can contain the JSON
212207
* itself or, if it ends with {@code .json}, the name of a resource to be
213208
* loaded from the classpath.
214209
* @param expected the expected JSON or the name of a resource containing
215210
* the expected JSON
216211
*/
217212
public SELF isLenientlyEqualTo(@Nullable CharSequence expected) {
218-
return isEqualTo(expected, JSONCompareMode.LENIENT);
213+
return isEqualTo(expected, JsonCompareMode.LENIENT);
219214
}
220215

221216
/**
222-
* Verify that the actual value is {@link JSONCompareMode#LENIENT leniently}
217+
* Verify that the actual value is {@link JsonCompareMode#LENIENT leniently}
223218
* equal to the given JSON {@link Resource}.
224219
* <p>The resource abstraction allows to provide several input types:
225220
* <ul>
@@ -231,23 +226,23 @@ public SELF isLenientlyEqualTo(@Nullable CharSequence expected) {
231226
* @param expected a resource containing the expected JSON
232227
*/
233228
public SELF isLenientlyEqualTo(Resource expected) {
234-
return isEqualTo(expected, JSONCompareMode.LENIENT);
229+
return isEqualTo(expected, JsonCompareMode.LENIENT);
235230
}
236231

237232
/**
238-
* Verify that the actual value is {@link JSONCompareMode#STRICT strictly}
233+
* Verify that the actual value is {@link JsonCompareMode#STRICT strictly}
239234
* equal to the given JSON. The {@code expected} value can contain the JSON
240235
* itself or, if it ends with {@code .json}, the name of a resource to be
241236
* loaded from the classpath.
242237
* @param expected the expected JSON or the name of a resource containing
243238
* the expected JSON
244239
*/
245240
public SELF isStrictlyEqualTo(@Nullable CharSequence expected) {
246-
return isEqualTo(expected, JSONCompareMode.STRICT);
241+
return isEqualTo(expected, JsonCompareMode.STRICT);
247242
}
248243

249244
/**
250-
* Verify that the actual value is {@link JSONCompareMode#STRICT strictly}
245+
* Verify that the actual value is {@link JsonCompareMode#STRICT strictly}
251246
* equal to the given JSON {@link Resource}.
252247
* <p>The resource abstraction allows to provide several input types:
253248
* <ul>
@@ -259,7 +254,7 @@ public SELF isStrictlyEqualTo(@Nullable CharSequence expected) {
259254
* @param expected a resource containing the expected JSON
260255
*/
261256
public SELF isStrictlyEqualTo(Resource expected) {
262-
return isEqualTo(expected, JSONCompareMode.STRICT);
257+
return isEqualTo(expected, JsonCompareMode.STRICT);
263258
}
264259

265260
/**
@@ -270,9 +265,9 @@ public SELF isStrictlyEqualTo(Resource expected) {
270265
* the expected JSON
271266
* @param compareMode the compare mode used when checking
272267
*/
273-
public SELF isNotEqualTo(@Nullable CharSequence expected, JSONCompareMode compareMode) {
268+
public SELF isNotEqualTo(@Nullable CharSequence expected, JsonCompareMode compareMode) {
274269
String expectedJson = this.jsonLoader.getJson(expected);
275-
return assertNotPassed(compare(expectedJson, compareMode));
270+
return assertIsMismatch(compare(expectedJson, compareMode));
276271
}
277272

278273
/**
@@ -287,9 +282,9 @@ public SELF isNotEqualTo(@Nullable CharSequence expected, JSONCompareMode compar
287282
* @param expected a resource containing the expected JSON
288283
* @param compareMode the compare mode used when checking
289284
*/
290-
public SELF isNotEqualTo(Resource expected, JSONCompareMode compareMode) {
285+
public SELF isNotEqualTo(Resource expected, JsonCompareMode compareMode) {
291286
String expectedJson = this.jsonLoader.getJson(expected);
292-
return assertNotPassed(compare(expectedJson, compareMode));
287+
return assertIsMismatch(compare(expectedJson, compareMode));
293288
}
294289

295290
/**
@@ -300,9 +295,9 @@ public SELF isNotEqualTo(Resource expected, JSONCompareMode compareMode) {
300295
* the expected JSON
301296
* @param comparator the comparator used when checking
302297
*/
303-
public SELF isNotEqualTo(@Nullable CharSequence expected, JSONComparator comparator) {
298+
public SELF isNotEqualTo(@Nullable CharSequence expected, JsonComparator comparator) {
304299
String expectedJson = this.jsonLoader.getJson(expected);
305-
return assertNotPassed(compare(expectedJson, comparator));
300+
return assertIsMismatch(compare(expectedJson, comparator));
306301
}
307302

308303
/**
@@ -317,25 +312,25 @@ public SELF isNotEqualTo(@Nullable CharSequence expected, JSONComparator compara
317312
* @param expected a resource containing the expected JSON
318313
* @param comparator the comparator used when checking
319314
*/
320-
public SELF isNotEqualTo(Resource expected, JSONComparator comparator) {
315+
public SELF isNotEqualTo(Resource expected, JsonComparator comparator) {
321316
String expectedJson = this.jsonLoader.getJson(expected);
322-
return assertNotPassed(compare(expectedJson, comparator));
317+
return assertIsMismatch(compare(expectedJson, comparator));
323318
}
324319

325320
/**
326-
* Verify that the actual value is not {@link JSONCompareMode#LENIENT
321+
* Verify that the actual value is not {@link JsonCompareMode#LENIENT
327322
* leniently} equal to the given JSON. The {@code expected} value can
328323
* contain the JSON itself or, if it ends with {@code .json}, the name of a
329324
* resource to be loaded from the classpath.
330325
* @param expected the expected JSON or the name of a resource containing
331326
* the expected JSON
332327
*/
333328
public SELF isNotLenientlyEqualTo(@Nullable CharSequence expected) {
334-
return isNotEqualTo(expected, JSONCompareMode.LENIENT);
329+
return isNotEqualTo(expected, JsonCompareMode.LENIENT);
335330
}
336331

337332
/**
338-
* Verify that the actual value is not {@link JSONCompareMode#LENIENT
333+
* Verify that the actual value is not {@link JsonCompareMode#LENIENT
339334
* leniently} equal to the given JSON {@link Resource}.
340335
* <p>The resource abstraction allows to provide several input types:
341336
* <ul>
@@ -347,23 +342,23 @@ public SELF isNotLenientlyEqualTo(@Nullable CharSequence expected) {
347342
* @param expected a resource containing the expected JSON
348343
*/
349344
public SELF isNotLenientlyEqualTo(Resource expected) {
350-
return isNotEqualTo(expected, JSONCompareMode.LENIENT);
345+
return isNotEqualTo(expected, JsonCompareMode.LENIENT);
351346
}
352347

353348
/**
354-
* Verify that the actual value is not {@link JSONCompareMode#STRICT
349+
* Verify that the actual value is not {@link JsonCompareMode#STRICT
355350
* strictly} equal to the given JSON. The {@code expected} value can
356351
* contain the JSON itself or, if it ends with {@code .json}, the name of a
357352
* resource to be loaded from the classpath.
358353
* @param expected the expected JSON or the name of a resource containing
359354
* the expected JSON
360355
*/
361356
public SELF isNotStrictlyEqualTo(@Nullable CharSequence expected) {
362-
return isNotEqualTo(expected, JSONCompareMode.STRICT);
357+
return isNotEqualTo(expected, JsonCompareMode.STRICT);
363358
}
364359

365360
/**
366-
* Verify that the actual value is not {@link JSONCompareMode#STRICT
361+
* Verify that the actual value is not {@link JsonCompareMode#STRICT
367362
* strictly} equal to the given JSON {@link Resource}.
368363
* <p>The resource abstraction allows to provide several input types:
369364
* <ul>
@@ -375,7 +370,7 @@ public SELF isNotStrictlyEqualTo(@Nullable CharSequence expected) {
375370
* @param expected a resource containing the expected JSON
376371
*/
377372
public SELF isNotStrictlyEqualTo(Resource expected) {
378-
return isNotEqualTo(expected, JSONCompareMode.STRICT);
373+
return isNotEqualTo(expected, JsonCompareMode.STRICT);
379374
}
380375

381376
/**
@@ -405,54 +400,25 @@ public SELF withCharset(@Nullable Charset charset) {
405400
}
406401

407402

408-
private JSONCompareResult compare(@Nullable CharSequence expectedJson, JSONCompareMode compareMode) {
409-
return compare(this.actual, expectedJson, (actualJsonString, expectedJsonString) ->
410-
JSONCompare.compareJSON(expectedJsonString, actualJsonString, compareMode));
403+
private JsonComparison compare(@Nullable CharSequence expectedJson, JsonCompareMode compareMode) {
404+
return compare(expectedJson, JsonAssert.comparator(compareMode));
411405
}
412406

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

418-
private JSONCompareResult compare(@Nullable CharSequence actualJson, @Nullable CharSequence expectedJson,
419-
ThrowingBiFunction<String, String, JSONCompareResult> comparator) {
420-
421-
if (actualJson == null) {
422-
return compareForNull(expectedJson);
423-
}
424-
if (expectedJson == null) {
425-
return compareForNull(actualJson.toString());
426-
}
427-
try {
428-
return comparator.applyWithException(actualJson.toString(), expectedJson.toString());
429-
}
430-
catch (Exception ex) {
431-
if (ex instanceof RuntimeException runtimeException) {
432-
throw runtimeException;
433-
}
434-
throw new IllegalStateException(ex);
435-
}
411+
private SELF assertIsMatch(JsonComparison result) {
412+
return assertComparison(result, JsonComparison.Result.MATCH);
436413
}
437414

438-
private JSONCompareResult compareForNull(@Nullable CharSequence expectedJson) {
439-
JSONCompareResult result = new JSONCompareResult();
440-
if (expectedJson != null) {
441-
result.fail("Expected null JSON");
442-
}
443-
return result;
444-
}
445-
446-
private SELF assertNotFailed(JSONCompareResult result) {
447-
if (result.failed()) {
448-
failWithMessage("JSON comparison failure: %s", result.getMessage());
449-
}
450-
return this.myself;
415+
private SELF assertIsMismatch(JsonComparison result) {
416+
return assertComparison(result, JsonComparison.Result.MISMATCH);
451417
}
452418

453-
private SELF assertNotPassed(JSONCompareResult result) {
454-
if (result.passed()) {
455-
failWithMessage("JSON comparison failure: %s", result.getMessage());
419+
private SELF assertComparison(JsonComparison jsonComparison, JsonComparison.Result requiredResult) {
420+
if (jsonComparison.getResult() != requiredResult) {
421+
failWithMessage("JSON comparison failure: %s", jsonComparison.getMessage());
456422
}
457423
return this.myself;
458424
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.json;
18+
19+
import org.skyscreamer.jsonassert.JSONCompare;
20+
import org.skyscreamer.jsonassert.JSONCompareMode;
21+
import org.skyscreamer.jsonassert.JSONCompareResult;
22+
import org.skyscreamer.jsonassert.comparator.JSONComparator;
23+
24+
import org.springframework.lang.Nullable;
25+
import org.springframework.util.function.ThrowingBiFunction;
26+
27+
/**
28+
* Useful methods that can be used with {@code org.skyscreamer.jsonassert}.
29+
*
30+
* @author Phillip Webb
31+
* @since 6.2
32+
*/
33+
public abstract class JsonAssert {
34+
35+
/**
36+
* Create a {@link JsonComparator} from the given {@link JsonCompareMode}.
37+
* @param compareMode the mode to use
38+
* @return a new {@link JsonComparator} instance
39+
* @see JSONCompareMode#STRICT
40+
* @see JSONCompareMode#LENIENT
41+
*/
42+
public static JsonComparator comparator(JsonCompareMode compareMode) {
43+
return comparator(toJSONCompareMode(compareMode));
44+
}
45+
46+
/**
47+
* Create a new {@link JsonComparator} from the given JSONAssert
48+
* {@link JSONComparator}.
49+
* @param comparator the JSON Assert {@link JSONComparator}
50+
* @return a new {@link JsonComparator} instance
51+
*/
52+
public static JsonComparator comparator(JSONComparator comparator) {
53+
return comparator((expectedJson, actualJson) -> JSONCompare
54+
.compareJSON(expectedJson, actualJson, comparator));
55+
}
56+
57+
/**
58+
* Create a new {@link JsonComparator} from the given JSONAssert
59+
* {@link JSONCompareMode}.
60+
* @param mode the JSON Assert {@link JSONCompareMode}
61+
* @return a new {@link JsonComparator} instance
62+
*/
63+
public static JsonComparator comparator(JSONCompareMode mode) {
64+
return comparator((expectedJson, actualJson) -> JSONCompare
65+
.compareJSON(expectedJson, actualJson, mode));
66+
}
67+
68+
private static JsonComparator comparator(ThrowingBiFunction<String, String, JSONCompareResult> compareFunction) {
69+
return (expectedJson, actualJson) -> compare(expectedJson, actualJson, compareFunction);
70+
}
71+
72+
private static JsonComparison compare(@Nullable String expectedJson, @Nullable String actualJson,
73+
ThrowingBiFunction<String, String, JSONCompareResult> compareFunction) {
74+
75+
if (actualJson == null) {
76+
return (expectedJson != null)
77+
? JsonComparison.mismatch("Expected null JSON")
78+
: JsonComparison.match();
79+
}
80+
if (expectedJson == null) {
81+
return JsonComparison.mismatch("Expected non-null JSON");
82+
}
83+
JSONCompareResult result = compareFunction.throwing(IllegalStateException::new).apply(expectedJson, actualJson);
84+
return (!result.passed())
85+
? JsonComparison.mismatch(result.getMessage())
86+
: JsonComparison.match();
87+
}
88+
89+
private static JSONCompareMode toJSONCompareMode(JsonCompareMode compareMode) {
90+
return (compareMode != JsonCompareMode.LENIENT ? JSONCompareMode.STRICT : JSONCompareMode.LENIENT);
91+
}
92+
93+
}

0 commit comments

Comments
 (0)