Add support for JSON assertions using JSON compare

This commit moves JSON content AssertJ support from Spring Boot.

See gh-21178

Co-authored-by: Brian Clozel <brian.clozel@broadcom.com>
This commit is contained in:
Stéphane Nicoll
2024-03-15 13:14:38 +01:00
parent 214a54dd6f
commit 97ebc43ea9
14 changed files with 1143 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
/*
* 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.assertj.core.api.AssertProvider;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* JSON content usually created from a JSON tester. Generally used only to
* {@link AssertProvider provide} {@link JsonContentAssert} to AssertJ
* {@code assertThat} calls.
*
* @author Phillip Webb
* @author Diego Berrueta
* @since 6.2
*/
public final class JsonContent implements AssertProvider<JsonContentAssert> {
private final String json;
@Nullable
private final Class<?> resourceLoadClass;
/**
* Create a new {@link JsonContent} instance.
* @param json the actual JSON content
* @param resourceLoadClass the source class used to load resources
*/
JsonContent(String json, @Nullable Class<?> resourceLoadClass) {
Assert.notNull(json, "JSON must not be null");
this.json = json;
this.resourceLoadClass = resourceLoadClass;
}
/**
* Use AssertJ's {@link org.assertj.core.api.Assertions#assertThat assertThat}
* instead.
*/
@Override
public JsonContentAssert assertThat() {
return new JsonContentAssert(this.json, this.resourceLoadClass, null);
}
/**
* Return the actual JSON content string.
* @return the JSON content
*/
public String getJson() {
return this.json;
}
@Override
public String toString() {
return "JsonContent " + this.json;
}
}

View File

@@ -0,0 +1,367 @@
/*
* 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 java.io.File;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.file.Path;
import org.assertj.core.api.AbstractAssert;
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;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.lang.Nullable;
import org.springframework.util.function.ThrowingBiFunction;
/**
* AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied
* to a {@link CharSequence} representation of a json document, mostly to
* compare the json document against a target, using {@linkplain JSONCompare
* JSON Assert}.
*
* @author Phillip Webb
* @author Andy Wilkinson
* @author Diego Berrueta
* @author Camille Vienot
* @author Stephane Nicoll
* @since 6.2
*/
public class JsonContentAssert extends AbstractAssert<JsonContentAssert, CharSequence> {
private final JsonLoader loader;
/**
* Create a new {@link JsonContentAssert} instance that will load resources
* relative to the given {@code resourceLoadClass}, using the given
* {@code charset}.
* @param json the actual JSON content
* @param resourceLoadClass the source class used to load resources
* @param charset the charset of the JSON resources
*/
public JsonContentAssert(@Nullable CharSequence json, @Nullable Class<?> resourceLoadClass,
@Nullable Charset charset) {
super(json, JsonContentAssert.class);
this.loader = new JsonLoader(resourceLoadClass, charset);
}
/**
* Create a new {@link JsonContentAssert} instance that will load resources
* relative to the given {@code resourceLoadClass}, using {@code UTF-8}.
* @param json the actual JSON content
* @param resourceLoadClass the source class used to load resources
*/
public JsonContentAssert(@Nullable CharSequence json, @Nullable Class<?> resourceLoadClass) {
this(json, resourceLoadClass, null);
}
/**
* Verify that the actual value is 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
* @param compareMode the compare mode used when checking
*/
public JsonContentAssert isEqualTo(@Nullable CharSequence expected, JSONCompareMode compareMode) {
String expectedJson = this.loader.getJson(expected);
return assertNotFailed(compare(expectedJson, compareMode));
}
/**
* Verify that the actual value is equal to the given JSON {@link Resource}.
* <p>The resource abstraction allows to provide several input types:
* <ul>
* <li>a {@code byte} array, using {@link ByteArrayResource}</li>
* <li>a {@code classpath} resource, using {@link ClassPathResource}</li>
* <li>a {@link File} or {@link Path}, using {@link FileSystemResource}</li>
* <li>an {@link InputStream}, using {@link InputStreamResource}</li>
* </ul>
* @param expected a resource containing the expected JSON
* @param compareMode the compare mode used when checking
*/
public JsonContentAssert isEqualTo(Resource expected, JSONCompareMode compareMode) {
String expectedJson = this.loader.getJson(expected);
return assertNotFailed(compare(expectedJson, compareMode));
}
/**
* Verify that the actual value is 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
* @param comparator the comparator used when checking
*/
public JsonContentAssert isEqualTo(@Nullable CharSequence expected, JSONComparator comparator) {
String expectedJson = this.loader.getJson(expected);
return assertNotFailed(compare(expectedJson, comparator));
}
/**
* Verify that the actual value is equal to the given JSON {@link Resource}.
* <p>The resource abstraction allows to provide several input types:
* <ul>
* <li>a {@code byte} array, using {@link ByteArrayResource}</li>
* <li>a {@code classpath} resource, using {@link ClassPathResource}</li>
* <li>a {@link File} or {@link Path}, using {@link FileSystemResource}</li>
* <li>an {@link InputStream}, using {@link InputStreamResource}</li>
* </ul>
* @param expected a resource containing the expected JSON
* @param comparator the comparator used when checking
*/
public JsonContentAssert isEqualTo(Resource expected, JSONComparator comparator) {
String expectedJson = this.loader.getJson(expected);
return assertNotFailed(compare(expectedJson, comparator));
}
/**
* 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 JsonContentAssert isLenientlyEqualTo(@Nullable CharSequence expected) {
return isEqualTo(expected, JSONCompareMode.LENIENT);
}
/**
* 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>
* <li>a {@code byte} array, using {@link ByteArrayResource}</li>
* <li>a {@code classpath} resource, using {@link ClassPathResource}</li>
* <li>a {@link File} or {@link Path}, using {@link FileSystemResource}</li>
* <li>an {@link InputStream}, using {@link InputStreamResource}</li>
* </ul>
* @param expected a resource containing the expected JSON
*/
public JsonContentAssert isLenientlyEqualTo(Resource expected) {
return isEqualTo(expected, JSONCompareMode.LENIENT);
}
/**
* 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 JsonContentAssert isStrictlyEqualTo(@Nullable CharSequence expected) {
return isEqualTo(expected, JSONCompareMode.STRICT);
}
/**
* 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>
* <li>a {@code byte} array, using {@link ByteArrayResource}</li>
* <li>a {@code classpath} resource, using {@link ClassPathResource}</li>
* <li>a {@link File} or {@link Path}, using {@link FileSystemResource}</li>
* <li>an {@link InputStream}, using {@link InputStreamResource}</li>
* </ul>
* @param expected a resource containing the expected JSON
*/
public JsonContentAssert isStrictlyEqualTo(Resource expected) {
return isEqualTo(expected, JSONCompareMode.STRICT);
}
/**
* Verify that the actual value is not 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
* @param compareMode the compare mode used when checking
*/
public JsonContentAssert isNotEqualTo(@Nullable CharSequence expected, JSONCompareMode compareMode) {
String expectedJson = this.loader.getJson(expected);
return assertNotPassed(compare(expectedJson, compareMode));
}
/**
* Verify that the actual value is not equal to the given JSON {@link Resource}.
* <p>The resource abstraction allows to provide several input types:
* <ul>
* <li>a {@code byte} array, using {@link ByteArrayResource}</li>
* <li>a {@code classpath} resource, using {@link ClassPathResource}</li>
* <li>a {@link File} or {@link Path}, using {@link FileSystemResource}</li>
* <li>an {@link InputStream}, using {@link InputStreamResource}</li>
* </ul>
* @param expected a resource containing the expected JSON
* @param compareMode the compare mode used when checking
*/
public JsonContentAssert isNotEqualTo(Resource expected, JSONCompareMode compareMode) {
String expectedJson = this.loader.getJson(expected);
return assertNotPassed(compare(expectedJson, compareMode));
}
/**
* Verify that the actual value is not 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
* @param comparator the comparator used when checking
*/
public JsonContentAssert isNotEqualTo(@Nullable CharSequence expected, JSONComparator comparator) {
String expectedJson = this.loader.getJson(expected);
return assertNotPassed(compare(expectedJson, comparator));
}
/**
* Verify that the actual value is not equal to the given JSON {@link Resource}.
* <p>The resource abstraction allows to provide several input types:
* <ul>
* <li>a {@code byte} array, using {@link ByteArrayResource}</li>
* <li>a {@code classpath} resource, using {@link ClassPathResource}</li>
* <li>a {@link File} or {@link Path}, using {@link FileSystemResource}</li>
* <li>an {@link InputStream}, using {@link InputStreamResource}</li>
* </ul>
* @param expected a resource containing the expected JSON
* @param comparator the comparator used when checking
*/
public JsonContentAssert isNotEqualTo(Resource expected, JSONComparator comparator) {
String expectedJson = this.loader.getJson(expected);
return assertNotPassed(compare(expectedJson, comparator));
}
/**
* 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 JsonContentAssert isNotLenientlyEqualTo(@Nullable CharSequence expected) {
return isNotEqualTo(expected, 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>
* <li>a {@code byte} array, using {@link ByteArrayResource}</li>
* <li>a {@code classpath} resource, using {@link ClassPathResource}</li>
* <li>a {@link File} or {@link Path}, using {@link FileSystemResource}</li>
* <li>an {@link InputStream}, using {@link InputStreamResource}</li>
* </ul>
* @param expected a resource containing the expected JSON
*/
public JsonContentAssert isNotLenientlyEqualTo(Resource expected) {
return isNotEqualTo(expected, JSONCompareMode.LENIENT);
}
/**
* 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 JsonContentAssert isNotStrictlyEqualTo(@Nullable CharSequence expected) {
return isNotEqualTo(expected, 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>
* <li>a {@code byte} array, using {@link ByteArrayResource}</li>
* <li>a {@code classpath} resource, using {@link ClassPathResource}</li>
* <li>a {@link File} or {@link Path}, using {@link FileSystemResource}</li>
* <li>an {@link InputStream}, using {@link InputStreamResource}</li>
* </ul>
* @param expected a resource containing the expected JSON
*/
public JsonContentAssert isNotStrictlyEqualTo(Resource expected) {
return isNotEqualTo(expected, JSONCompareMode.STRICT);
}
private JSONCompareResult compare(@Nullable CharSequence expectedJson, JSONCompareMode compareMode) {
return compare(this.actual, expectedJson, (actualJsonString, expectedJsonString) ->
JSONCompare.compareJSON(expectedJsonString, actualJsonString, compareMode));
}
private JSONCompareResult compare(@Nullable CharSequence expectedJson, JSONComparator comparator) {
return compare(this.actual, expectedJson, (actualJsonString, expectedJsonString) ->
JSONCompare.compareJSON(expectedJsonString, actualJsonString, comparator));
}
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 JSONCompareResult compareForNull(@Nullable CharSequence expectedJson) {
JSONCompareResult result = new JSONCompareResult();
result.passed();
if (expectedJson != null) {
result.fail("Expected null JSON");
}
return result;
}
private JsonContentAssert assertNotFailed(JSONCompareResult result) {
if (result.failed()) {
failWithMessage("JSON Comparison failure: %s", result.getMessage());
}
return this;
}
private JsonContentAssert assertNotPassed(JSONCompareResult result) {
if (result.passed()) {
failWithMessage("JSON Comparison failure: %s", result.getMessage());
}
return this;
}
}

View File

@@ -0,0 +1,74 @@
/*
* 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 java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.lang.Nullable;
import org.springframework.util.FileCopyUtils;
/**
* Internal helper used to load JSON from various sources.
*
* @author Phillip Webb
* @author Andy Wilkinson
* @author Stephane Nicoll
* @since 6.2
*/
class JsonLoader {
@Nullable
private final Class<?> resourceLoadClass;
private final Charset charset;
JsonLoader(@Nullable Class<?> resourceLoadClass, @Nullable Charset charset) {
this.resourceLoadClass = resourceLoadClass;
this.charset = (charset != null ? charset : StandardCharsets.UTF_8);
}
@Nullable
String getJson(@Nullable CharSequence source) {
if (source == null) {
return null;
}
if (source.toString().endsWith(".json")) {
return getJson(new ClassPathResource(source.toString(), this.resourceLoadClass));
}
return source.toString();
}
String getJson(Resource source) {
try {
return getJson(source.getInputStream());
}
catch (IOException ex) {
throw new IllegalStateException("Unable to load JSON from " + source, ex);
}
}
private String getJson(InputStream source) throws IOException {
return FileCopyUtils.copyToString(new InputStreamReader(source, this.charset));
}
}

View File

@@ -0,0 +1,9 @@
/**
* Testing support for JSON.
*/
@NonNullApi
@NonNullFields
package org.springframework.test.json;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;