Allow JsonPathExpectationsHelper to use a custom configuration

This commit improves JsonPathExpectationsHelper to allow a custom json
path configuration to be defined. The primary objective of a custom
configuration is to specify a custom mapper that can deserialize complex
object structure.

As part of this commit, it is now possible to invoke a Matcher against
a type that holds generic information, using a regular
ParameterizedTypeReference.

Given that the existing constructor takes a vararg of Object, this
commit also deprecates this constructor in favor of formatting the
expression String upfront.

Closes gh-31651
This commit is contained in:
Stéphane Nicoll
2024-01-15 15:22:46 +01:00
parent 4f8eca1350
commit 9f8038963f
5 changed files with 172 additions and 24 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* 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.
@@ -16,14 +16,21 @@
package org.springframework.test.util;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.TypeRef;
import com.jayway.jsonpath.spi.mapper.MappingProvider;
import org.hamcrest.CoreMatchers;
import org.hamcrest.Matcher;
import org.hamcrest.MatcherAssert;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@@ -40,6 +47,7 @@ import org.springframework.util.StringUtils;
* @author Juergen Hoeller
* @author Craig Andrews
* @author Sam Brannen
* @author Stephane Nicoll
* @since 3.2
*/
public class JsonPathExpectationsHelper {
@@ -48,17 +56,42 @@ public class JsonPathExpectationsHelper {
private final JsonPath jsonPath;
private final Configuration configuration;
/**
* Construct a new {@code JsonPathExpectationsHelper}.
* @param expression the {@link JsonPath} expression; never {@code null} or empty
* @param configuration the {@link Configuration} to use or {@code null} to use the
* {@linkplain Configuration#defaultConfiguration() default configuration}
* @since 6.2
*/
public JsonPathExpectationsHelper(String expression, @Nullable Configuration configuration) {
Assert.hasText(expression, "expression must not be null or empty");
this.expression = expression;
this.jsonPath = JsonPath.compile(this.expression);
this.configuration = (configuration != null) ? configuration : Configuration.defaultConfiguration();
}
/**
* Construct a new {@code JsonPathExpectationsHelper} using the
* {@linkplain Configuration#defaultConfiguration() default configuration}.
* @param expression the {@link JsonPath} expression; never {@code null} or empty
* @since 6.2
*/
public JsonPathExpectationsHelper(String expression) {
this(expression, (Configuration) null);
}
/**
* Construct a new {@code JsonPathExpectationsHelper}.
* @param expression the {@link JsonPath} expression; never {@code null} or empty
* @param args arguments to parameterize the {@code JsonPath} expression with,
* using formatting specifiers defined in {@link String#format(String, Object...)}
* @deprecated in favor of calling {@link String#formatted(Object...)} upfront
*/
@Deprecated(since = "6.2", forRemoval = true)
public JsonPathExpectationsHelper(String expression, Object... args) {
Assert.hasText(expression, "expression must not be null or empty");
this.expression = String.format(expression, args);
this.jsonPath = JsonPath.compile(this.expression);
this(expression.formatted(args), (Configuration) null);
}
@@ -83,9 +116,25 @@ public class JsonPathExpectationsHelper {
* @param targetType the expected type of the resulting value
* @since 4.3.3
*/
@SuppressWarnings("unchecked")
public <T> void assertValue(String content, Matcher<? super T> matcher, Class<T> targetType) {
T value = (T) evaluateJsonPath(content, targetType);
T value = evaluateJsonPath(content, targetType);
MatcherAssert.assertThat("JSON path \"" + this.expression + "\"", value, matcher);
}
/**
* An overloaded variant of {@link #assertValue(String, Matcher)} that also
* accepts a target type for the resulting value that allows generic types
* to be defined.
* <p>This must be used with a {@link Configuration} that defines a more
* elaborate {@link MappingProvider} as the default one cannot handle
* generic types.
* @param content the JSON content
* @param matcher the matcher with which to assert the result
* @param targetType the expected type of the resulting value
* @since 6.2
*/
public <T> void assertValue(String content, Matcher<? super T> matcher, ParameterizedTypeReference<T> targetType) {
T value = evaluateJsonPath(content, targetType);
MatcherAssert.assertThat("JSON path \"" + this.expression + "\"", value, matcher);
}
@@ -296,7 +345,7 @@ public class JsonPathExpectationsHelper {
@Nullable
public Object evaluateJsonPath(String content) {
try {
return this.jsonPath.read(content);
return this.jsonPath.read(content, this.configuration);
}
catch (Throwable ex) {
throw new AssertionError("No value at JSON path \"" + this.expression + "\"", ex);
@@ -306,19 +355,32 @@ public class JsonPathExpectationsHelper {
/**
* Variant of {@link #evaluateJsonPath(String)} with a target type.
* <p>This can be useful for matching numbers reliably for example coercing an
* integer into a double.
* integer into a double or when the configured {@link MappingProvider} can
* handle more complex object structures.
* @param content the content to evaluate against
* @param targetType the requested target type
* @return the result of the evaluation
* @throws AssertionError if the evaluation fails
*/
public Object evaluateJsonPath(String content, Class<?> targetType) {
try {
return JsonPath.parse(content).read(this.expression, targetType);
}
catch (Throwable ex) {
String message = "No value at JSON path \"" + this.expression + "\"";
throw new AssertionError(message, ex);
}
public <T> T evaluateJsonPath(String content, Class<T> targetType) {
return evaluateExpression(content, context -> context.read(this.expression, targetType));
}
/**
* Variant of {@link #evaluateJsonPath(String)} with a target type that has
* generics.
* <p>This must be used with a {@link Configuration} that defines a more
* elaborate {@link MappingProvider} as the default one cannot handle
* generic types.
* @param content the content to evaluate against
* @param targetType the requested target type
* @return the result of the evaluation
* @throws AssertionError if the evaluation fails
* @since 6.2
*/
public <T> T evaluateJsonPath(String content, ParameterizedTypeReference<T> targetType) {
return evaluateExpression(content, context ->
context.read(this.expression, new TypeRefAdapter<>(targetType)));
}
@Nullable
@@ -336,4 +398,33 @@ public class JsonPathExpectationsHelper {
return !this.jsonPath.isDefinite();
}
private <T> T evaluateExpression(String content, Function<DocumentContext, T> action) {
try {
DocumentContext context = JsonPath.parse(content, this.configuration);
return action.apply(context);
}
catch (Throwable ex) {
String message = "Failed to evaluate JSON path \"" + this.expression + "\"";
throw new AssertionError(message, ex);
}
}
/**
* Adapt JSONPath {@link TypeRef} to {@link ParameterizedTypeReference}.
*/
private static final class TypeRefAdapter<T> extends TypeRef<T> {
private final Type type;
TypeRefAdapter(ParameterizedTypeReference<T> typeReference) {
this.type = typeReference.getType();
}
@Override
public Type getType() {
return this.type;
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* 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.
@@ -26,6 +26,7 @@ import org.springframework.http.client.ClientHttpRequest;
import org.springframework.mock.http.client.MockClientHttpRequest;
import org.springframework.test.util.JsonPathExpectationsHelper;
import org.springframework.test.web.client.RequestMatcher;
import org.springframework.util.Assert;
/**
* Factory for assertions on the request content using
@@ -53,7 +54,8 @@ public class JsonPathRequestMatchers {
* using formatting specifiers defined in {@link String#format(String, Object...)}
*/
protected JsonPathRequestMatchers(String expression, Object... args) {
this.jsonPathHelper = new JsonPathExpectationsHelper(expression, args);
Assert.hasText(expression, "expression must not be null or empty");
this.jsonPathHelper = new JsonPathExpectationsHelper(expression.formatted(args));
}

View File

@@ -22,6 +22,7 @@ import org.hamcrest.Matcher;
import org.springframework.lang.Nullable;
import org.springframework.test.util.JsonPathExpectationsHelper;
import org.springframework.util.Assert;
/**
* <a href="https://github.com/jayway/JsonPath">JsonPath</a> assertions.
@@ -41,9 +42,10 @@ public class JsonPathAssertions {
JsonPathAssertions(WebTestClient.BodyContentSpec spec, String content, String expression, Object... args) {
Assert.hasText(expression, "expression must not be null or empty");
this.bodySpec = spec;
this.content = content;
this.pathHelper = new JsonPathExpectationsHelper(expression, args);
this.pathHelper = new JsonPathExpectationsHelper(expression.formatted(args));
}
@@ -181,10 +183,9 @@ public class JsonPathAssertions {
* Consume the result of the JSONPath evaluation and provide a target class.
* @since 6.2
*/
@SuppressWarnings("unchecked")
public <T> WebTestClient.BodyContentSpec value(Class<T> targetType, Consumer<T> consumer) {
Object value = this.pathHelper.evaluateJsonPath(this.content, targetType);
consumer.accept((T) value);
T value = this.pathHelper.evaluateJsonPath(this.content, targetType);
consumer.accept(value);
return this.bodySpec;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* 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.
@@ -28,6 +28,7 @@ import org.springframework.lang.Nullable;
import org.springframework.test.util.JsonPathExpectationsHelper;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultMatcher;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
@@ -60,7 +61,8 @@ public class JsonPathResultMatchers {
* using formatting specifiers defined in {@link String#format(String, Object...)}
*/
protected JsonPathResultMatchers(String expression, Object... args) {
this.jsonPathHelper = new JsonPathExpectationsHelper(expression, args);
Assert.hasText(expression, "expression must not be null or empty");
this.jsonPathHelper = new JsonPathExpectationsHelper(expression.formatted(args));
}
/**