Browse Source
This commit moves JSON path AssertJ support from Spring Boot. See gh-21178 Co-authored-by: Brian Clozel <brian.clozel@broadcom.com>pull/32467/head
5 changed files with 1103 additions and 0 deletions
@ -0,0 +1,235 @@
@@ -0,0 +1,235 @@
|
||||
/* |
||||
* 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.lang.reflect.Array; |
||||
import java.lang.reflect.Type; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
|
||||
import org.assertj.core.api.AbstractBooleanAssert; |
||||
import org.assertj.core.api.AbstractMapAssert; |
||||
import org.assertj.core.api.AbstractObjectAssert; |
||||
import org.assertj.core.api.AbstractStringAssert; |
||||
import org.assertj.core.api.Assertions; |
||||
import org.assertj.core.api.ObjectArrayAssert; |
||||
import org.assertj.core.error.BasicErrorMessageFactory; |
||||
import org.assertj.core.internal.Failures; |
||||
|
||||
import org.springframework.core.ParameterizedTypeReference; |
||||
import org.springframework.core.ResolvableType; |
||||
import org.springframework.http.HttpInputMessage; |
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.http.converter.GenericHttpMessageConverter; |
||||
import org.springframework.lang.Nullable; |
||||
import org.springframework.mock.http.MockHttpInputMessage; |
||||
import org.springframework.mock.http.MockHttpOutputMessage; |
||||
import org.springframework.util.ObjectUtils; |
||||
import org.springframework.util.StringUtils; |
||||
|
||||
/** |
||||
* Base AssertJ {@link org.assertj.core.api.Assert assertions} that can be |
||||
* applied to a JSON value. In JSON, values must be one of the following data |
||||
* types: |
||||
* <ul> |
||||
* <li>a {@linkplain #asString() string}</li> |
||||
* <li>a {@linkplain #asNumber() number}</li> |
||||
* <li>a {@linkplain #asBoolean() boolean}</li> |
||||
* <li>an {@linkplain #asArray() array}</li> |
||||
* <li>an {@linkplain #asMap() object} (JSON object)</li> |
||||
* <li>{@linkplain #isNull() null}</li> |
||||
* </ul> |
||||
* This base class offers direct access for each of those types as well as a |
||||
* conversion methods based on an optional {@link GenericHttpMessageConverter}. |
||||
* |
||||
* @author Stephane Nicoll |
||||
* @since 6.2 |
||||
* @param <SELF> the type of assertions |
||||
*/ |
||||
public abstract class AbstractJsonValueAssert<SELF extends AbstractJsonValueAssert<SELF>> |
||||
extends AbstractObjectAssert<SELF, Object> { |
||||
|
||||
private final Failures failures = Failures.instance(); |
||||
|
||||
@Nullable |
||||
private final GenericHttpMessageConverter<Object> httpMessageConverter; |
||||
|
||||
|
||||
protected AbstractJsonValueAssert(@Nullable Object actual, Class<?> selfType, |
||||
@Nullable GenericHttpMessageConverter<Object> httpMessageConverter) { |
||||
super(actual, selfType); |
||||
this.httpMessageConverter = httpMessageConverter; |
||||
} |
||||
|
||||
/** |
||||
* Verify that the actual value is a non-{@code null} {@link String} |
||||
* and return a new {@linkplain AbstractStringAssert assertion} object that |
||||
* provides dedicated {@code String} assertions for it. |
||||
*/ |
||||
@Override |
||||
public AbstractStringAssert<?> asString() { |
||||
return Assertions.assertThat(castTo(String.class, "a string")); |
||||
} |
||||
|
||||
/** |
||||
* Verify that the actual value is a non-{@code null} {@link Number}, |
||||
* usually an {@link Integer} or {@link Double} and return a new |
||||
* {@linkplain AbstractObjectAssert assertion} object for it. |
||||
*/ |
||||
public AbstractObjectAssert<?, Number> asNumber() { |
||||
return Assertions.assertThat(castTo(Number.class, "a number")); |
||||
} |
||||
|
||||
/** |
||||
* Verify that the actual value is a non-{@code null} {@link Boolean} |
||||
* and return a new {@linkplain AbstractBooleanAssert assertion} object |
||||
* that provides dedicated {@code Boolean} assertions for it. |
||||
*/ |
||||
public AbstractBooleanAssert<?> asBoolean() { |
||||
return Assertions.assertThat(castTo(Boolean.class, "a boolean")); |
||||
} |
||||
|
||||
/** |
||||
* Verify that the actual value is a non-{@code null} {@link Array} |
||||
* and return a new {@linkplain ObjectArrayAssert assertion} object |
||||
* that provides dedicated {@code Array} assertions for it. |
||||
*/ |
||||
public ObjectArrayAssert<Object> asArray() { |
||||
List<?> list = castTo(List.class, "an array"); |
||||
Object[] array = list.toArray(new Object[0]); |
||||
return Assertions.assertThat(array); |
||||
} |
||||
|
||||
/** |
||||
* Verify that the actual value is a non-{@code null} JSON object and |
||||
* return a new {@linkplain AbstractMapAssert assertion} object that |
||||
* provides dedicated assertions on individual elements of the |
||||
* object. The returned map assertion object uses the attribute name as the |
||||
* key, and the value can itself be any of the valid JSON values. |
||||
*/ |
||||
@SuppressWarnings("unchecked") |
||||
public AbstractMapAssert<?, Map<String, Object>, String, Object> asMap() { |
||||
return Assertions.assertThat(castTo(Map.class, "a map")); |
||||
} |
||||
|
||||
private <T> T castTo(Class<T> expectedType, String description) { |
||||
if (this.actual == null) { |
||||
throw valueProcessingFailed("To be %s%n".formatted(description)); |
||||
} |
||||
if (!expectedType.isInstance(this.actual)) { |
||||
throw valueProcessingFailed("To be %s%nBut was:%n %s%n".formatted(description, this.actual.getClass().getName())); |
||||
} |
||||
return expectedType.cast(this.actual); |
||||
} |
||||
|
||||
/** |
||||
* Verify that the actual value can be converted to an instance of the |
||||
* given {@code target} and produce a new {@linkplain AbstractObjectAssert |
||||
* assertion} object narrowed to that type. |
||||
* @param target the {@linkplain Class type} to convert the actual value to |
||||
*/ |
||||
public <T> AbstractObjectAssert<?, T> convertTo(Class<T> target) { |
||||
isNotNull(); |
||||
T value = convertToTargetType(target); |
||||
return Assertions.assertThat(value); |
||||
} |
||||
|
||||
/** |
||||
* Verify that the actual value can be converted to an instance of the |
||||
* given {@code target} and produce a new {@linkplain AbstractObjectAssert |
||||
* assertion} object narrowed to that type. |
||||
* @param target the {@linkplain ParameterizedTypeReference parameterized |
||||
* type} to convert the actual value to |
||||
*/ |
||||
public <T> AbstractObjectAssert<?, T> convertTo(ParameterizedTypeReference<T> target) { |
||||
isNotNull(); |
||||
T value = convertToTargetType(target.getType()); |
||||
return Assertions.assertThat(value); |
||||
} |
||||
|
||||
/** |
||||
* Verify that the actual value is empty, that is a {@code null} scalar |
||||
* value or an empty list or map. Can also be used when the path is using a |
||||
* filter operator to validate that it dit not match. |
||||
*/ |
||||
public SELF isEmpty() { |
||||
if (!ObjectUtils.isEmpty(this.actual)) { |
||||
throw valueProcessingFailed("To be empty"); |
||||
} |
||||
return this.myself; |
||||
} |
||||
|
||||
/** |
||||
* Verify that the actual value is not empty, that is a non-{@code null} |
||||
* scalar value or a non-empty list or map. Can also be used when the path is |
||||
* using a filter operator to validate that it dit match at least one |
||||
* element. |
||||
*/ |
||||
public SELF isNotEmpty() { |
||||
if (ObjectUtils.isEmpty(this.actual)) { |
||||
throw valueProcessingFailed("To not be empty"); |
||||
} |
||||
return this.myself; |
||||
} |
||||
|
||||
|
||||
@SuppressWarnings("unchecked") |
||||
private <T> T convertToTargetType(Type targetType) { |
||||
if (this.httpMessageConverter == null) { |
||||
throw new IllegalStateException( |
||||
"No JSON message converter available to convert %s".formatted(actualToString())); |
||||
} |
||||
try { |
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); |
||||
this.httpMessageConverter.write(this.actual, ResolvableType.forInstance(this.actual).getType(), |
||||
MediaType.APPLICATION_JSON, outputMessage); |
||||
return (T) this.httpMessageConverter.read(targetType, getClass(), |
||||
fromHttpOutputMessage(outputMessage)); |
||||
} |
||||
catch (Exception ex) { |
||||
throw valueProcessingFailed("To convert successfully to:%n %s%nBut it failed:%n %s%n" |
||||
.formatted(targetType.getTypeName(), ex.getMessage())); |
||||
} |
||||
} |
||||
|
||||
private HttpInputMessage fromHttpOutputMessage(MockHttpOutputMessage message) { |
||||
MockHttpInputMessage inputMessage = new MockHttpInputMessage(message.getBodyAsBytes()); |
||||
inputMessage.getHeaders().addAll(message.getHeaders()); |
||||
return inputMessage; |
||||
} |
||||
|
||||
protected String getExpectedErrorMessagePrefix() { |
||||
return "Expected:"; |
||||
} |
||||
|
||||
private AssertionError valueProcessingFailed(String errorMessage) { |
||||
throw this.failures.failure(this.info, new ValueProcessingFailed( |
||||
getExpectedErrorMessagePrefix(), actualToString(), errorMessage)); |
||||
} |
||||
|
||||
private String actualToString() { |
||||
return ObjectUtils.nullSafeToString(StringUtils.quoteIfString(this.actual)); |
||||
} |
||||
|
||||
private static final class ValueProcessingFailed extends BasicErrorMessageFactory { |
||||
|
||||
private ValueProcessingFailed(String prefix, String actualToString, String errorMessage) { |
||||
super("%n%s%n %s%n%s".formatted(prefix, actualToString, errorMessage)); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,165 @@
@@ -0,0 +1,165 @@
|
||||
/* |
||||
* 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.util.function.Consumer; |
||||
|
||||
import com.jayway.jsonpath.JsonPath; |
||||
import com.jayway.jsonpath.PathNotFoundException; |
||||
import org.assertj.core.api.AbstractAssert; |
||||
import org.assertj.core.api.AssertProvider; |
||||
import org.assertj.core.error.BasicErrorMessageFactory; |
||||
import org.assertj.core.internal.Failures; |
||||
|
||||
import org.springframework.http.converter.GenericHttpMessageConverter; |
||||
import org.springframework.lang.Nullable; |
||||
import org.springframework.util.Assert; |
||||
|
||||
/** |
||||
* AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied |
||||
* to a {@link CharSequence} representation of a json document using |
||||
* {@linkplain JsonPath JSON path}. |
||||
* |
||||
* @author Stephane Nicoll |
||||
* @since 6.2 |
||||
*/ |
||||
public class JsonPathAssert extends AbstractAssert<JsonPathAssert, CharSequence> { |
||||
|
||||
private static final Failures failures = Failures.instance(); |
||||
|
||||
@Nullable |
||||
private final GenericHttpMessageConverter<Object> jsonMessageConverter; |
||||
|
||||
public JsonPathAssert(CharSequence json, |
||||
@Nullable GenericHttpMessageConverter<Object> jsonMessageConverter) { |
||||
super(json, JsonPathAssert.class); |
||||
this.jsonMessageConverter = jsonMessageConverter; |
||||
} |
||||
|
||||
/** |
||||
* Verify that the given JSON {@code path} is present and extract the JSON |
||||
* value for further {@linkplain JsonPathValueAssert assertions}. |
||||
* @param path the {@link JsonPath} expression |
||||
* @see #hasPathSatisfying(String, Consumer) |
||||
*/ |
||||
public JsonPathValueAssert extractingPath(String path) { |
||||
Object value = new JsonPathValue(path).getValue(); |
||||
return new JsonPathValueAssert(value, path, this.jsonMessageConverter); |
||||
} |
||||
|
||||
/** |
||||
* Verify that the given JSON {@code path} is present with a JSON value |
||||
* satisfying the given {@code valueRequirements}. |
||||
* @param path the {@link JsonPath} expression |
||||
* @param valueRequirements a {@link Consumer} of the assertion object |
||||
*/ |
||||
public JsonPathAssert hasPathSatisfying(String path, Consumer<AssertProvider<JsonPathValueAssert>> valueRequirements) { |
||||
Object value = new JsonPathValue(path).assertHasPath(); |
||||
JsonPathValueAssert valueAssert = new JsonPathValueAssert(value, path, this.jsonMessageConverter); |
||||
valueRequirements.accept(() -> valueAssert); |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Verify that the given JSON {@code path} matches. For paths with an |
||||
* operator, this validates that the path expression is valid, but does not |
||||
* validate that it yield any results. |
||||
* @param path the {@link JsonPath} expression |
||||
*/ |
||||
public JsonPathAssert hasPath(String path) { |
||||
new JsonPathValue(path).assertHasPath(); |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Verify that the given JSON {@code path} does not match. |
||||
* @param path the {@link JsonPath} expression |
||||
*/ |
||||
public JsonPathAssert doesNotHavePath(String path) { |
||||
new JsonPathValue(path).assertDoesNotHavePath(); |
||||
return this; |
||||
} |
||||
|
||||
|
||||
private AssertionError failure(BasicErrorMessageFactory errorMessageFactory) { |
||||
throw failures.failure(this.info, errorMessageFactory); |
||||
} |
||||
|
||||
|
||||
/** |
||||
* A {@link JsonPath} value. |
||||
*/ |
||||
private class JsonPathValue { |
||||
|
||||
private final String path; |
||||
|
||||
private final JsonPath jsonPath; |
||||
|
||||
private final String json; |
||||
|
||||
JsonPathValue(String path) { |
||||
Assert.hasText(path, "'path' must not be null or empty"); |
||||
this.path = path; |
||||
this.jsonPath = JsonPath.compile(this.path); |
||||
this.json = JsonPathAssert.this.actual.toString(); |
||||
} |
||||
|
||||
@Nullable |
||||
Object assertHasPath() { |
||||
return getValue(); |
||||
} |
||||
|
||||
void assertDoesNotHavePath() { |
||||
try { |
||||
read(); |
||||
throw failure(new JsonPathNotExpected(this.json, this.path)); |
||||
} |
||||
catch (PathNotFoundException ignore) { |
||||
} |
||||
} |
||||
|
||||
@Nullable |
||||
Object getValue() { |
||||
try { |
||||
return read(); |
||||
} |
||||
catch (PathNotFoundException ex) { |
||||
throw failure(new JsonPathNotFound(this.json, this.path)); |
||||
} |
||||
} |
||||
|
||||
@Nullable |
||||
private Object read() { |
||||
return this.jsonPath.read(this.json); |
||||
} |
||||
|
||||
|
||||
static final class JsonPathNotFound extends BasicErrorMessageFactory { |
||||
|
||||
private JsonPathNotFound(String actual, String path) { |
||||
super("%nExpecting:%n %s%nTo match JSON path:%n %s%n", actual, path); |
||||
} |
||||
} |
||||
|
||||
static final class JsonPathNotExpected extends BasicErrorMessageFactory { |
||||
|
||||
private JsonPathNotExpected(String actual, String path) { |
||||
super("%nExpecting:%n %s%nTo not match JSON path:%n %s%n", actual, path); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,48 @@
@@ -0,0 +1,48 @@
|
||||
/* |
||||
* 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 com.jayway.jsonpath.JsonPath; |
||||
|
||||
import org.springframework.http.converter.GenericHttpMessageConverter; |
||||
import org.springframework.lang.Nullable; |
||||
|
||||
/** |
||||
* AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied |
||||
* to a JSON value produced by evaluating a {@linkplain JsonPath JSON path} |
||||
* expression. |
||||
* |
||||
* @author Stephane Nicoll |
||||
* @since 6.2 |
||||
*/ |
||||
public class JsonPathValueAssert |
||||
extends AbstractJsonValueAssert<JsonPathValueAssert> { |
||||
|
||||
private final String expression; |
||||
|
||||
|
||||
JsonPathValueAssert(@Nullable Object actual, String expression, |
||||
@Nullable GenericHttpMessageConverter<Object> httpMessageConverter) { |
||||
super(actual, JsonPathValueAssert.class, httpMessageConverter); |
||||
this.expression = expression; |
||||
} |
||||
|
||||
@Override |
||||
protected String getExpectedErrorMessagePrefix() { |
||||
return "Expected value at JSON path \"%s\":".formatted(this.expression); |
||||
} |
||||
} |
||||
@ -0,0 +1,322 @@
@@ -0,0 +1,322 @@
|
||||
/* |
||||
* 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.util.List; |
||||
import java.util.Map; |
||||
import java.util.function.Consumer; |
||||
import java.util.stream.Stream; |
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
import org.assertj.core.api.AssertProvider; |
||||
import org.junit.jupiter.api.Nested; |
||||
import org.junit.jupiter.api.Test; |
||||
import org.junit.jupiter.params.ParameterizedTest; |
||||
import org.junit.jupiter.params.provider.Arguments; |
||||
import org.junit.jupiter.params.provider.MethodSource; |
||||
import org.junit.jupiter.params.provider.ValueSource; |
||||
|
||||
import org.springframework.core.ParameterizedTypeReference; |
||||
import org.springframework.core.io.ClassPathResource; |
||||
import org.springframework.http.converter.GenericHttpMessageConverter; |
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; |
||||
import org.springframework.lang.Nullable; |
||||
import org.springframework.util.FileCopyUtils; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; |
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException; |
||||
import static org.assertj.core.api.Assertions.entry; |
||||
|
||||
/** |
||||
* Tests for {@link JsonPathAssert}. |
||||
* |
||||
* @author Phillip Webb |
||||
* @author Stephane Nicoll |
||||
*/ |
||||
class JsonPathAssertTests { |
||||
|
||||
private static final String TYPES = loadJson("types.json"); |
||||
|
||||
private static final String SIMPSONS = loadJson("simpsons.json"); |
||||
|
||||
private static final String NULLS = loadJson("nulls.json"); |
||||
|
||||
private static final MappingJackson2HttpMessageConverter jsonHttpMessageConverter = |
||||
new MappingJackson2HttpMessageConverter(new ObjectMapper()); |
||||
|
||||
|
||||
@Nested |
||||
class HasPathTests { |
||||
|
||||
@Test |
||||
void hasPathForPresentAndNotNull() { |
||||
assertThat(forJson(NULLS)).hasPath("$.valuename"); |
||||
} |
||||
|
||||
@Test |
||||
void hasPathForPresentAndNull() { |
||||
assertThat(forJson(NULLS)).hasPath("$.nullname"); |
||||
} |
||||
|
||||
@Test |
||||
void hasPathForOperatorMatching() { |
||||
assertThat(forJson(SIMPSONS)). |
||||
hasPath("$.familyMembers[?(@.name == 'Homer')]"); |
||||
} |
||||
|
||||
@Test |
||||
void hasPathForOperatorNotMatching() { |
||||
assertThat(forJson(SIMPSONS)). |
||||
hasPath("$.familyMembers[?(@.name == 'Dilbert')]"); |
||||
} |
||||
|
||||
@Test |
||||
void hasPathForNotPresent() { |
||||
String expression = "$.missing"; |
||||
assertThatExceptionOfType(AssertionError.class) |
||||
.isThrownBy(() -> assertThat(forJson(NULLS)).hasPath(expression)) |
||||
.satisfies(hasFailedToMatchPath("$.missing")); |
||||
} |
||||
|
||||
@Test |
||||
void hasPathSatisfying() { |
||||
assertThat(forJson(TYPES)).hasPathSatisfying("$.str", value -> assertThat(value).isEqualTo("foo")) |
||||
.hasPathSatisfying("$.num", value -> assertThat(value).isEqualTo(5)); |
||||
} |
||||
|
||||
@Test |
||||
void hasPathSatisfyingForPathNotPresent() { |
||||
String expression = "missing"; |
||||
assertThatExceptionOfType(AssertionError.class) |
||||
.isThrownBy(() -> assertThat(forJson(NULLS)).hasPathSatisfying(expression, value -> {})) |
||||
.satisfies(hasFailedToMatchPath(expression)); |
||||
} |
||||
|
||||
@Test |
||||
void doesNotHavePathForMissing() { |
||||
assertThat(forJson(NULLS)).doesNotHavePath("$.missing"); |
||||
} |
||||
|
||||
|
||||
@Test |
||||
void doesNotHavePathForPresent() { |
||||
String expression = "$.valuename"; |
||||
assertThatExceptionOfType(AssertionError.class) |
||||
.isThrownBy(() -> assertThat(forJson(NULLS)).doesNotHavePath(expression)) |
||||
.satisfies(hasFailedToNotMatchPath(expression)); |
||||
} |
||||
} |
||||
|
||||
|
||||
@Nested |
||||
class ExtractingPathTests { |
||||
|
||||
@Test |
||||
void isNullWithNullPathValue() { |
||||
assertThat(forJson(NULLS)).extractingPath("$.nullname").isNull(); |
||||
} |
||||
|
||||
@ParameterizedTest |
||||
@ValueSource(strings = { "$.str", "$.emptyString", "$.num", "$.bool", "$.arr", |
||||
"$.emptyArray", "$.colorMap", "$.emptyMap" }) |
||||
void isNotNullWithValue(String path) { |
||||
assertThat(forJson(TYPES)).extractingPath(path).isNotNull(); |
||||
} |
||||
|
||||
@ParameterizedTest |
||||
@MethodSource |
||||
void isEqualToOnRawValue(String path, Object expected) { |
||||
assertThat(forJson(TYPES)).extractingPath(path).isEqualTo(expected); |
||||
} |
||||
|
||||
static Stream<Arguments> isEqualToOnRawValue() { |
||||
return Stream.of( |
||||
Arguments.of("$.str", "foo"), |
||||
Arguments.of("$.num", 5), |
||||
Arguments.of("$.bool", true), |
||||
Arguments.of("$.arr", List.of(42)), |
||||
Arguments.of("$.colorMap", Map.of("red", "rojo"))); |
||||
} |
||||
|
||||
@Test |
||||
void asStringWithActualValue() { |
||||
assertThat(forJson(TYPES)).extractingPath("@.str").asString().startsWith("f").endsWith("o"); |
||||
} |
||||
|
||||
@Test |
||||
void asStringIsEmpty() { |
||||
assertThat(forJson(TYPES)).extractingPath("@.emptyString").asString().isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void asNumberWithActualValue() { |
||||
assertThat(forJson(TYPES)).extractingPath("@.num").asNumber().isEqualTo(5); |
||||
} |
||||
|
||||
@Test |
||||
void asBooleanWithActualValue() { |
||||
assertThat(forJson(TYPES)).extractingPath("@.bool").asBoolean().isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
void asArrayWithActualValue() { |
||||
assertThat(forJson(TYPES)).extractingPath("@.arr").asArray().containsOnly(42); |
||||
} |
||||
|
||||
@Test |
||||
void asArrayIsEmpty() { |
||||
assertThat(forJson(TYPES)).extractingPath("@.emptyArray").asArray().isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void asArrayWithFilterPredicatesMatching() { |
||||
assertThat(forJson(SIMPSONS)) |
||||
.extractingPath("$.familyMembers[?(@.name == 'Bart')]").asArray().hasSize(1); |
||||
} |
||||
|
||||
@Test |
||||
void asArrayWithFilterPredicatesNotMatching() { |
||||
assertThat(forJson(SIMPSONS)). |
||||
extractingPath("$.familyMembers[?(@.name == 'Dilbert')]").asArray().isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void asMapWithActualValue() { |
||||
assertThat(forJson(TYPES)).extractingPath("@.colorMap").asMap().containsOnly(entry("red", "rojo")); |
||||
} |
||||
|
||||
@Test |
||||
void asMapIsEmpty() { |
||||
assertThat(forJson(TYPES)).extractingPath("@.emptyMap").asMap().isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void convertToWithoutHttpMessageConverterShouldFail() { |
||||
JsonPathValueAssert path = assertThat(forJson(SIMPSONS)).extractingPath("$.familyMembers[0]"); |
||||
assertThatIllegalStateException().isThrownBy(() -> path.convertTo(Member.class)) |
||||
.withMessage("No JSON message converter available to convert {name=Homer}"); |
||||
} |
||||
|
||||
@Test |
||||
void convertToTargetType() { |
||||
assertThat(forJson(SIMPSONS, jsonHttpMessageConverter)) |
||||
.extractingPath("$.familyMembers[0]").convertTo(Member.class) |
||||
.satisfies(member -> assertThat(member.name).isEqualTo("Homer")); |
||||
} |
||||
|
||||
@Test |
||||
void convertToIncompatibleTargetTypeShouldFail() { |
||||
JsonPathValueAssert path = assertThat(forJson(SIMPSONS, jsonHttpMessageConverter)) |
||||
.extractingPath("$.familyMembers[0]"); |
||||
assertThatExceptionOfType(AssertionError.class) |
||||
.isThrownBy(() -> path.convertTo(Customer.class)) |
||||
.withMessageContainingAll("Expected value at JSON path \"$.familyMembers[0]\":", |
||||
Customer.class.getName(), "name"); |
||||
} |
||||
|
||||
@Test |
||||
void convertArrayToParameterizedType() { |
||||
assertThat(forJson(SIMPSONS, jsonHttpMessageConverter)) |
||||
.extractingPath("$.familyMembers") |
||||
.convertTo(new ParameterizedTypeReference<List<Member>>() {}) |
||||
.satisfies(family -> assertThat(family).hasSize(5).element(0).isEqualTo(new Member("Homer"))); |
||||
} |
||||
|
||||
@Test |
||||
void isEmptyWithPathHavingNullValue() { |
||||
assertThat(forJson(NULLS)).extractingPath("nullname").isEmpty(); |
||||
} |
||||
|
||||
@ParameterizedTest |
||||
@ValueSource(strings = { "$.emptyString", "$.emptyArray", "$.emptyMap" }) |
||||
void isEmptyWithEmptyValue(String path) { |
||||
assertThat(forJson(TYPES)).extractingPath(path).isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void isEmptyForPathWithFilterMatching() { |
||||
String expression = "$.familyMembers[?(@.name == 'Bart')]"; |
||||
assertThatExceptionOfType(AssertionError.class) |
||||
.isThrownBy(() -> assertThat(forJson(SIMPSONS)).extractingPath(expression).isEmpty()) |
||||
.withMessageContainingAll("Expected value at JSON path \"" + expression + "\"", |
||||
"[{\"name\":\"Bart\"}]", "To be empty"); |
||||
} |
||||
|
||||
@Test |
||||
void isEmptyForPathWithFilterNotMatching() { |
||||
assertThat(forJson(SIMPSONS)).extractingPath("$.familyMembers[?(@.name == 'Dilbert')]").isEmpty(); |
||||
} |
||||
|
||||
@ParameterizedTest |
||||
@ValueSource(strings = { "$.str", "$.num", "$.bool", "$.arr", "$.colorMap" }) |
||||
void isNotEmptyWithNonNullValue(String path) { |
||||
assertThat(forJson(TYPES)).extractingPath(path).isNotEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void isNotEmptyForPathWithFilterMatching() { |
||||
assertThat(forJson(SIMPSONS)).extractingPath("$.familyMembers[?(@.name == 'Bart')]").isNotEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void isNotEmptyForPathWithFilterNotMatching() { |
||||
String expression = "$.familyMembers[?(@.name == 'Dilbert')]"; |
||||
assertThatExceptionOfType(AssertionError.class) |
||||
.isThrownBy(() -> assertThat(forJson(SIMPSONS)).extractingPath(expression).isNotEmpty()) |
||||
.withMessageContainingAll("Expected value at JSON path \"" + expression + "\"", |
||||
"To not be empty"); |
||||
} |
||||
|
||||
|
||||
private record Member(String name) {} |
||||
|
||||
private record Customer(long id, String username) {} |
||||
|
||||
} |
||||
|
||||
private Consumer<AssertionError> hasFailedToMatchPath(String expression) { |
||||
return error -> assertThat(error.getMessage()).containsSubsequence("Expecting:", |
||||
"To match JSON path:", "\"" + expression + "\""); |
||||
} |
||||
|
||||
private Consumer<AssertionError> hasFailedToNotMatchPath(String expression) { |
||||
return error -> assertThat(error.getMessage()).containsSubsequence("Expecting:", |
||||
"To not match JSON path:", "\"" + expression + "\""); |
||||
} |
||||
|
||||
|
||||
private static String loadJson(String path) { |
||||
try { |
||||
ClassPathResource resource = new ClassPathResource(path, JsonPathAssertTests.class); |
||||
return new String(FileCopyUtils.copyToByteArray(resource.getInputStream())); |
||||
} |
||||
catch (Exception ex) { |
||||
throw new IllegalStateException(ex); |
||||
} |
||||
} |
||||
|
||||
private AssertProvider<JsonPathAssert> forJson(String json) { |
||||
return forJson(json, null); |
||||
} |
||||
|
||||
private AssertProvider<JsonPathAssert> forJson(String json, |
||||
@Nullable GenericHttpMessageConverter<Object> jsonHttpMessageConverter) { |
||||
return () -> new JsonPathAssert(json, jsonHttpMessageConverter); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,333 @@
@@ -0,0 +1,333 @@
|
||||
/* |
||||
* 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.util.Collections; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.function.Consumer; |
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
import org.assertj.core.api.AssertProvider; |
||||
import org.assertj.core.api.InstanceOfAssertFactories; |
||||
import org.assertj.core.data.Offset; |
||||
import org.junit.jupiter.api.Nested; |
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.core.ParameterizedTypeReference; |
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; |
||||
import org.springframework.lang.Nullable; |
||||
import org.springframework.util.StringUtils; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; |
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException; |
||||
|
||||
/** |
||||
* Tests for {@link JsonPathValueAssert}. |
||||
* |
||||
* @author Stephane Nicoll |
||||
*/ |
||||
class JsonPathValueAssertTests { |
||||
|
||||
@Nested |
||||
class AsStringTests { |
||||
|
||||
@Test |
||||
void asStringWithStringValue() { |
||||
assertThat(forValue("test")).asString().isEqualTo("test"); |
||||
} |
||||
|
||||
@Test |
||||
void asStringWithEmptyValue() { |
||||
assertThat(forValue("")).asString().isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void asStringWithNonStringFails() { |
||||
int value = 123; |
||||
AssertProvider<JsonPathValueAssert> actual = forValue(value); |
||||
assertThatExceptionOfType(AssertionError.class) |
||||
.isThrownBy(() -> assertThat(actual).asString().isEqualTo("123")) |
||||
.satisfies(hasFailedToBeOfType(value, "a string")); |
||||
} |
||||
|
||||
@Test |
||||
void asStringWithNullFails() { |
||||
AssertProvider<JsonPathValueAssert> actual = forValue(null); |
||||
assertThatExceptionOfType(AssertionError.class) |
||||
.isThrownBy(() -> assertThat(actual).asString().isEqualTo("null")) |
||||
.satisfies(hasFailedToBeOfTypeWhenNull("a string")); |
||||
} |
||||
} |
||||
|
||||
@Nested |
||||
class AsNumberTests { |
||||
|
||||
@Test |
||||
void asNumberWithIntegerValue() { |
||||
assertThat(forValue(123)).asNumber().isEqualTo(123); |
||||
} |
||||
|
||||
@Test |
||||
void asNumberWithDoubleValue() { |
||||
assertThat(forValue(3.1415926)).asNumber() |
||||
.asInstanceOf(InstanceOfAssertFactories.DOUBLE) |
||||
.isEqualTo(3.14, Offset.offset(0.01)); |
||||
} |
||||
|
||||
@Test |
||||
void asNumberWithNonNumberFails() { |
||||
String value = "123"; |
||||
AssertProvider<JsonPathValueAssert> actual = forValue(value); |
||||
assertThatExceptionOfType(AssertionError.class) |
||||
.isThrownBy(() -> assertThat(actual).asNumber().isEqualTo(123)) |
||||
.satisfies(hasFailedToBeOfType(value, "a number")); |
||||
} |
||||
|
||||
@Test |
||||
void asNumberWithNullFails() { |
||||
AssertProvider<JsonPathValueAssert> actual = forValue(null); |
||||
assertThatExceptionOfType(AssertionError.class) |
||||
.isThrownBy(() -> assertThat(actual).asNumber().isEqualTo(0)) |
||||
.satisfies(hasFailedToBeOfTypeWhenNull("a number")); |
||||
} |
||||
} |
||||
|
||||
@Nested |
||||
class AsBooleanTests { |
||||
|
||||
@Test |
||||
void asBooleanWithBooleanPrimitiveValue() { |
||||
assertThat(forValue(true)).asBoolean().isEqualTo(true); |
||||
} |
||||
|
||||
@Test |
||||
void asBooleanWithBooleanWrapperValue() { |
||||
assertThat(forValue(Boolean.FALSE)).asBoolean().isEqualTo(false); |
||||
} |
||||
|
||||
@Test |
||||
void asBooleanWithNonBooleanFails() { |
||||
String value = "false"; |
||||
AssertProvider<JsonPathValueAssert> actual = forValue(value); |
||||
assertThatExceptionOfType(AssertionError.class) |
||||
.isThrownBy(() -> assertThat(actual).asBoolean().isEqualTo(false)) |
||||
.satisfies(hasFailedToBeOfType(value, "a boolean")); |
||||
} |
||||
|
||||
@Test |
||||
void asBooleanWithNullFails() { |
||||
AssertProvider<JsonPathValueAssert> actual = forValue(null); |
||||
assertThatExceptionOfType(AssertionError.class) |
||||
.isThrownBy(() -> assertThat(actual).asBoolean().isEqualTo(false)) |
||||
.satisfies(hasFailedToBeOfTypeWhenNull("a boolean")); |
||||
} |
||||
} |
||||
|
||||
@Nested |
||||
class AsArrayTests { // json path uses List for arrays
|
||||
|
||||
@Test |
||||
void asArrayWithStringValues() { |
||||
assertThat(forValue(List.of("a", "b", "c"))).asArray().contains("a", "c"); |
||||
} |
||||
|
||||
@Test |
||||
void asArrayWithEmptyArray() { |
||||
assertThat(forValue(Collections.emptyList())).asArray().isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void asArrayWithNonArrayFails() { |
||||
String value = "test"; |
||||
AssertProvider<JsonPathValueAssert> actual = forValue(value); |
||||
assertThatExceptionOfType(AssertionError.class) |
||||
.isThrownBy(() -> assertThat(actual).asArray().contains("t")) |
||||
.satisfies(hasFailedToBeOfType(value, "an array")); |
||||
} |
||||
|
||||
@Test |
||||
void asArrayWithNullFails() { |
||||
AssertProvider<JsonPathValueAssert> actual = forValue(null); |
||||
assertThatExceptionOfType(AssertionError.class) |
||||
.isThrownBy(() -> assertThat(actual).asArray().isEqualTo(false)) |
||||
.satisfies(hasFailedToBeOfTypeWhenNull("an array")); |
||||
} |
||||
} |
||||
|
||||
@Nested |
||||
class AsMapTests { |
||||
|
||||
@Test |
||||
void asMapWithMapValue() { |
||||
assertThat(forValue(Map.of("zero", 0, "one", 1))).asMap().containsKeys("zero", "one") |
||||
.containsValues(0, 1); |
||||
} |
||||
|
||||
@Test |
||||
void asArrayWithEmptyMap() { |
||||
assertThat(forValue(Collections.emptyMap())).asMap().isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void asMapWithNonMapFails() { |
||||
List<String> value = List.of("a", "b"); |
||||
AssertProvider<JsonPathValueAssert> actual = forValue(value); |
||||
assertThatExceptionOfType(AssertionError.class) |
||||
.isThrownBy(() -> assertThat(actual).asMap().containsKey("a")) |
||||
.satisfies(hasFailedToBeOfType(value, "a map")); |
||||
} |
||||
|
||||
@Test |
||||
void asMapWithNullFails() { |
||||
AssertProvider<JsonPathValueAssert> actual = forValue(null); |
||||
assertThatExceptionOfType(AssertionError.class) |
||||
.isThrownBy(() -> assertThat(actual).asMap().isEmpty()) |
||||
.satisfies(hasFailedToBeOfTypeWhenNull("a map")); |
||||
} |
||||
} |
||||
|
||||
@Nested |
||||
class ConvertToTests { |
||||
|
||||
private static final MappingJackson2HttpMessageConverter jsonHttpMessageConverter = |
||||
new MappingJackson2HttpMessageConverter(new ObjectMapper()); |
||||
|
||||
@Test |
||||
void convertToWithoutHttpMessageConverter() { |
||||
AssertProvider<JsonPathValueAssert> actual = () -> new JsonPathValueAssert("123", "$.test", null); |
||||
assertThatIllegalStateException().isThrownBy(() -> assertThat(actual).convertTo(Integer.class)) |
||||
.withMessage("No JSON message converter available to convert '123'"); |
||||
} |
||||
|
||||
@Test |
||||
void convertObjectToPojo() { |
||||
assertThat(forValue(Map.of("id", 1234, "name", "John", "active", true))).convertTo(User.class) |
||||
.satisfies(user -> { |
||||
assertThat(user.id).isEqualTo(1234); |
||||
assertThat(user.name).isEqualTo("John"); |
||||
assertThat(user.active).isTrue(); |
||||
}); |
||||
} |
||||
|
||||
@Test |
||||
void convertArrayToListOfPojo() { |
||||
Map<?, ?> user1 = Map.of("id", 1234, "name", "John", "active", true); |
||||
Map<?, ?> user2 = Map.of("id", 5678, "name", "Sarah", "active", false); |
||||
Map<?, ?> user3 = Map.of("id", 9012, "name", "Sophia", "active", true); |
||||
assertThat(forValue(List.of(user1, user2, user3))) |
||||
.convertTo(new ParameterizedTypeReference<List<User>>() {}) |
||||
.satisfies(users -> assertThat(users).hasSize(3).extracting("name") |
||||
.containsExactly("John", "Sarah", "Sophia")); |
||||
} |
||||
|
||||
@Test |
||||
void convertObjectToPojoWithMissingMandatoryField() { |
||||
Map<?, ?> value = Map.of("firstName", "John"); |
||||
AssertProvider<JsonPathValueAssert> actual = forValue(value); |
||||
assertThatExceptionOfType(AssertionError.class) |
||||
.isThrownBy(() -> assertThat(actual).convertTo(User.class)) |
||||
.satisfies(hasFailedToConvertToType(value, User.class)) |
||||
.withMessageContaining("firstName"); |
||||
} |
||||
|
||||
|
||||
private AssertProvider<JsonPathValueAssert> forValue(@Nullable Object actual) { |
||||
return () -> new JsonPathValueAssert(actual, "$.test", jsonHttpMessageConverter); |
||||
} |
||||
|
||||
|
||||
private record User(long id, String name, boolean active) {} |
||||
|
||||
} |
||||
|
||||
@Nested |
||||
class EmptyNotEmptyTests { |
||||
|
||||
@Test |
||||
void isEmptyWithEmptyString() { |
||||
assertThat(forValue("")).isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void isEmptyWithNull() { |
||||
assertThat(forValue(null)).isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void isEmptyWithEmptyArray() { |
||||
assertThat(forValue(Collections.emptyList())).isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void isEmptyWithEmptyObject() { |
||||
assertThat(forValue(Collections.emptyMap())).isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void isEmptyWithWhitespace() { |
||||
AssertProvider<JsonPathValueAssert> actual = forValue(" "); |
||||
assertThatExceptionOfType(AssertionError.class) |
||||
.isThrownBy(() -> assertThat(actual).isEmpty()) |
||||
.satisfies(hasFailedEmptyCheck(" ")); |
||||
} |
||||
|
||||
@Test |
||||
void isNotEmptyWithString() { |
||||
assertThat(forValue("test")).isNotEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void isNotEmptyWithArray() { |
||||
assertThat(forValue(List.of("test"))).isNotEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void isNotEmptyWithObject() { |
||||
assertThat(forValue(Map.of("test", "value"))).isNotEmpty(); |
||||
} |
||||
|
||||
private Consumer<AssertionError> hasFailedEmptyCheck(Object actual) { |
||||
return error -> assertThat(error.getMessage()).containsSubsequence("Expected value at JSON path \"$.test\":", |
||||
"" + StringUtils.quoteIfString(actual), "To be empty"); |
||||
} |
||||
} |
||||
|
||||
|
||||
private Consumer<AssertionError> hasFailedToBeOfType(Object actual, String expectedDescription) { |
||||
return error -> assertThat(error.getMessage()).containsSubsequence("Expected value at JSON path \"$.test\":", |
||||
"" + StringUtils.quoteIfString(actual), "To be " + expectedDescription, "But was:", actual.getClass().getName()); |
||||
} |
||||
|
||||
private Consumer<AssertionError> hasFailedToBeOfTypeWhenNull(String expectedDescription) { |
||||
return error -> assertThat(error.getMessage()).containsSubsequence("Expected value at JSON path \"$.test\":", "null", |
||||
"To be " + expectedDescription); |
||||
} |
||||
|
||||
private Consumer<AssertionError> hasFailedToConvertToType(Object actual, Class<?> targetType) { |
||||
return error -> assertThat(error.getMessage()).containsSubsequence("Expected value at JSON path \"$.test\":", |
||||
"" + StringUtils.quoteIfString(actual), "To convert successfully to:", targetType.getTypeName(), "But it failed:"); |
||||
} |
||||
|
||||
|
||||
|
||||
private AssertProvider<JsonPathValueAssert> forValue(@Nullable Object actual) { |
||||
return () -> new JsonPathValueAssert(actual, "$.test", null); |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue