Configure jsonpath MappingProvider in WebTestClient

This commit improves jsonpath support in WebTestClient by detecting
a suitable json encoder/decoder that can be applied to assert more
complex data structure.

Closes gh-31653
This commit is contained in:
Stéphane Nicoll 2024-01-16 16:04:04 +01:00
parent 9f8038963f
commit e73bbd4ad3
10 changed files with 481 additions and 58 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 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.
@ -30,6 +30,8 @@ import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import java.util.function.Function;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.spi.mapper.MappingProvider;
import org.hamcrest.Matcher;
import org.hamcrest.MatcherAssert;
import org.reactivestreams.Publisher;
@ -57,6 +59,7 @@ import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.ExchangeFunction;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.util.UriBuilder;
import org.springframework.web.util.UriBuilderFactory;
@ -72,6 +75,9 @@ class DefaultWebTestClient implements WebTestClient {
private final WiretapConnector wiretapConnector;
@Nullable
private final JsonEncoderDecoder jsonEncoderDecoder;
private final ExchangeFunction exchangeFunction;
private final UriBuilderFactory uriBuilderFactory;
@ -91,13 +97,15 @@ class DefaultWebTestClient implements WebTestClient {
private final AtomicLong requestIndex = new AtomicLong();
DefaultWebTestClient(ClientHttpConnector connector,
DefaultWebTestClient(ClientHttpConnector connector, ExchangeStrategies exchangeStrategies,
Function<ClientHttpConnector, ExchangeFunction> exchangeFactory, UriBuilderFactory uriBuilderFactory,
@Nullable HttpHeaders headers, @Nullable MultiValueMap<String, String> cookies,
Consumer<EntityExchangeResult<?>> entityResultConsumer,
@Nullable Duration responseTimeout, DefaultWebTestClientBuilder clientBuilder) {
this.wiretapConnector = new WiretapConnector(connector);
this.jsonEncoderDecoder = JsonEncoderDecoder.from(
exchangeStrategies.messageWriters(), exchangeStrategies.messageReaders());
this.exchangeFunction = exchangeFactory.apply(this.wiretapConnector);
this.uriBuilderFactory = uriBuilderFactory;
this.defaultHeaders = headers;
@ -362,6 +370,7 @@ class DefaultWebTestClient implements WebTestClient {
this.requestId, this.uriTemplate, getResponseTimeout());
return new DefaultResponseSpec(result, response,
DefaultWebTestClient.this.jsonEncoderDecoder,
DefaultWebTestClient.this.entityResultConsumer, getResponseTimeout());
}
@ -399,6 +408,9 @@ class DefaultWebTestClient implements WebTestClient {
private final ClientResponse response;
@Nullable
private final JsonEncoderDecoder jsonEncoderDecoder;
private final Consumer<EntityExchangeResult<?>> entityResultConsumer;
private final Duration timeout;
@ -406,11 +418,13 @@ class DefaultWebTestClient implements WebTestClient {
DefaultResponseSpec(
ExchangeResult exchangeResult, ClientResponse response,
@Nullable JsonEncoderDecoder jsonEncoderDecoder,
Consumer<EntityExchangeResult<?>> entityResultConsumer,
Duration timeout) {
this.exchangeResult = exchangeResult;
this.response = response;
this.jsonEncoderDecoder = jsonEncoderDecoder;
this.entityResultConsumer = entityResultConsumer;
this.timeout = timeout;
}
@ -466,7 +480,7 @@ class DefaultWebTestClient implements WebTestClient {
ByteArrayResource resource = this.response.bodyToMono(ByteArrayResource.class).block(this.timeout);
byte[] body = (resource != null ? resource.getByteArray() : null);
EntityExchangeResult<byte[]> entityResult = initEntityExchangeResult(body);
return new DefaultBodyContentSpec(entityResult);
return new DefaultBodyContentSpec(entityResult, this.jsonEncoderDecoder);
}
private <B> EntityExchangeResult<B> initEntityExchangeResult(@Nullable B body) {
@ -625,10 +639,14 @@ class DefaultWebTestClient implements WebTestClient {
private final EntityExchangeResult<byte[]> result;
@Nullable
private final JsonEncoderDecoder jsonEncoderDecoder;
private final boolean isEmpty;
DefaultBodyContentSpec(EntityExchangeResult<byte[]> result) {
DefaultBodyContentSpec(EntityExchangeResult<byte[]> result, @Nullable JsonEncoderDecoder jsonEncoderDecoder) {
this.result = result;
this.jsonEncoderDecoder = jsonEncoderDecoder;
this.isEmpty = (result.getResponseBody() == null || result.getResponseBody().length == 0);
}
@ -666,8 +684,16 @@ class DefaultWebTestClient implements WebTestClient {
}
@Override
public JsonPathAssertions jsonPath(String expression) {
return new JsonPathAssertions(this, getBodyAsString(), expression,
JsonPathConfigurationProvider.getConfiguration(this.jsonEncoderDecoder));
}
@Override
@SuppressWarnings("removal")
public JsonPathAssertions jsonPath(String expression, Object... args) {
return new JsonPathAssertions(this, getBodyAsString(), expression, args);
Assert.hasText(expression, "expression must not be null or empty");
return jsonPath(expression.formatted(args));
}
@Override
@ -697,4 +723,18 @@ class DefaultWebTestClient implements WebTestClient {
}
}
private static class JsonPathConfigurationProvider {
static Configuration getConfiguration(@Nullable JsonEncoderDecoder jsonEncoderDecoder) {
Configuration jsonPathConfiguration = Configuration.defaultConfiguration();
if (jsonEncoderDecoder != null) {
MappingProvider mappingProvider = new EncoderDecoderMappingProvider(
jsonEncoderDecoder.encoder(), jsonEncoderDecoder.decoder());
return jsonPathConfiguration.mappingProvider(mappingProvider);
}
return jsonPathConfiguration;
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 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.
@ -294,8 +294,9 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder {
if (connectorToUse == null) {
connectorToUse = initConnector();
}
ExchangeStrategies exchangeStrategies = initExchangeStrategies();
Function<ClientHttpConnector, ExchangeFunction> exchangeFactory = connector -> {
ExchangeFunction exchange = ExchangeFunctions.create(connector, initExchangeStrategies());
ExchangeFunction exchange = ExchangeFunctions.create(connector, exchangeStrategies);
if (CollectionUtils.isEmpty(this.filters)) {
return exchange;
}
@ -305,7 +306,7 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder {
.orElse(exchange);
};
return new DefaultWebTestClient(connectorToUse, exchangeFactory, initUriBuilderFactory(),
return new DefaultWebTestClient(connectorToUse, exchangeStrategies, exchangeFactory, initUriBuilderFactory(),
this.defaultHeaders != null ? HttpHeaders.readOnlyHttpHeaders(this.defaultHeaders) : null,
this.defaultCookies != null ? CollectionUtils.unmodifiableMultiValueMap(this.defaultCookies) : null,
this.entityResultConsumer, this.responseTimeout, new DefaultWebTestClientBuilder(this));

View File

@ -0,0 +1,84 @@
/*
* 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.web.reactive.server;
import java.util.Collections;
import java.util.Map;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.TypeRef;
import com.jayway.jsonpath.spi.mapper.MappingProvider;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.Decoder;
import org.springframework.core.codec.Encoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.lang.Nullable;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
/**
* JSON Path {@link MappingProvider} implementation using {@link Encoder}
* and {@link Decoder}.
*
* @author Rossen Stoyanchev
* @author Stephane Nicoll
* @since 6.2
*/
final class EncoderDecoderMappingProvider implements MappingProvider {
private final Encoder<?> encoder;
private final Decoder<?> decoder;
/**
* Create an instance with the specified writers and readers.
*/
public EncoderDecoderMappingProvider(Encoder<?> encoder, Decoder<?> decoder) {
this.encoder = encoder;
this.decoder = decoder;
}
@Nullable
@Override
public <T> T map(Object source, Class<T> targetType, Configuration configuration) {
return mapToTargetType(source, ResolvableType.forClass(targetType));
}
@Nullable
@Override
public <T> T map(Object source, TypeRef<T> targetType, Configuration configuration) {
return mapToTargetType(source, ResolvableType.forType(targetType.getType()));
}
@SuppressWarnings("unchecked")
@Nullable
private <T> T mapToTargetType(Object source, ResolvableType targetType) {
DataBufferFactory bufferFactory = DefaultDataBufferFactory.sharedInstance;
MimeType mimeType = MimeTypeUtils.APPLICATION_JSON;
Map<String, Object> hints = Collections.emptyMap();
DataBuffer buffer = ((Encoder<T>) this.encoder).encodeValue(
(T) source, bufferFactory, ResolvableType.forInstance(source), mimeType, hints);
return ((Decoder<T>) this.decoder).decode(buffer, targetType, mimeType, hints);
}
}

View File

@ -0,0 +1,112 @@
/*
* 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.web.reactive.server;
import java.util.Collection;
import java.util.Map;
import java.util.stream.Stream;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.Decoder;
import org.springframework.core.codec.Encoder;
import org.springframework.http.MediaType;
import org.springframework.http.codec.DecoderHttpMessageReader;
import org.springframework.http.codec.EncoderHttpMessageWriter;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.lang.Nullable;
/**
* {@link Encoder} and {@link Decoder} that is able to handle a map to and from
* json. Used to configure the jsonpath infrastructure without having a hard
* dependency on the library.
*
* @param encoder the json encoder
* @param decoder the json decoder
* @author Stephane Nicoll
* @author Rossen Stoyanchev
* @since 6.2
*/
record JsonEncoderDecoder(Encoder<?> encoder, Decoder<?> decoder) {
private static final ResolvableType MAP_TYPE = ResolvableType.forClass(Map.class);
/**
* Create a {@link JsonEncoderDecoder} instance based on the specified
* infrastructure.
* @param messageWriters the HTTP message writers
* @param messageReaders the HTTP message readers
* @return a {@link JsonEncoderDecoder} or {@code null} if a suitable codec
* is not available
*/
@Nullable
static JsonEncoderDecoder from(Collection<HttpMessageWriter<?>> messageWriters,
Collection<HttpMessageReader<?>> messageReaders) {
Encoder<?> jsonEncoder = findJsonEncoder(messageWriters);
Decoder<?> jsonDecoder = findJsonDecoder(messageReaders);
if (jsonEncoder != null && jsonDecoder != null) {
return new JsonEncoderDecoder(jsonEncoder, jsonDecoder);
}
return null;
}
/**
* Find the first suitable {@link Encoder} that can encode a {@link Map}
* to json.
* @param writers the writers to inspect
* @return a suitable json {@link Encoder} or {@code null}
*/
@Nullable
private static Encoder<?> findJsonEncoder(Collection<HttpMessageWriter<?>> writers) {
return findJsonEncoder(writers.stream()
.filter(writer -> writer instanceof EncoderHttpMessageWriter)
.map(writer -> ((EncoderHttpMessageWriter<?>) writer).getEncoder()));
}
@Nullable
private static Encoder<?> findJsonEncoder(Stream<Encoder<?>> stream) {
return stream
.filter(encoder -> encoder.canEncode(MAP_TYPE, MediaType.APPLICATION_JSON))
.findFirst()
.orElse(null);
}
/**
* Find the first suitable {@link Decoder} that can decode a {@link Map} to
* json.
* @param readers the readers to inspect
* @return a suitable json {@link Decoder} or {@code null}
*/
@Nullable
private static Decoder<?> findJsonDecoder(Collection<HttpMessageReader<?>> readers) {
return findJsonDecoder(readers.stream()
.filter(reader -> reader instanceof DecoderHttpMessageReader)
.map(reader -> ((DecoderHttpMessageReader<?>) reader).getDecoder()));
}
@Nullable
private static Decoder<?> findJsonDecoder(Stream<Decoder<?>> decoderStream) {
return decoderStream
.filter(decoder -> decoder.canDecode(MAP_TYPE, MediaType.APPLICATION_JSON))
.findFirst()
.orElse(null);
}
}

View File

@ -18,8 +18,10 @@ package org.springframework.test.web.reactive.server;
import java.util.function.Consumer;
import com.jayway.jsonpath.Configuration;
import org.hamcrest.Matcher;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.lang.Nullable;
import org.springframework.test.util.JsonPathExpectationsHelper;
import org.springframework.util.Assert;
@ -28,6 +30,7 @@ import org.springframework.util.Assert;
* <a href="https://github.com/jayway/JsonPath">JsonPath</a> assertions.
*
* @author Rossen Stoyanchev
* @author Stephane Nicoll
* @since 5.0
* @see <a href="https://github.com/jayway/JsonPath">https://github.com/jayway/JsonPath</a>
* @see JsonPathExpectationsHelper
@ -41,11 +44,12 @@ public class JsonPathAssertions {
private final JsonPathExpectationsHelper pathHelper;
JsonPathAssertions(WebTestClient.BodyContentSpec spec, String content, String expression, Object... args) {
JsonPathAssertions(WebTestClient.BodyContentSpec spec, String content, String expression,
@Nullable Configuration configuration) {
Assert.hasText(expression, "expression must not be null or empty");
this.bodySpec = spec;
this.content = content;
this.pathHelper = new JsonPathExpectationsHelper(expression.formatted(args));
this.pathHelper = new JsonPathExpectationsHelper(expression, configuration);
}
@ -168,6 +172,15 @@ public class JsonPathAssertions {
return this.bodySpec;
}
/**
* Delegates to {@link JsonPathExpectationsHelper#assertValue(String, Matcher, ParameterizedTypeReference)}.
* @since 6.2
*/
public <T> WebTestClient.BodyContentSpec value(ParameterizedTypeReference<T> targetType, Matcher<? super T> matcher) {
this.pathHelper.assertValue(this.content, matcher, targetType);
return this.bodySpec;
}
/**
* Consume the result of the JSONPath evaluation.
* @since 5.1
@ -199,6 +212,16 @@ public class JsonPathAssertions {
return value(targetType, consumer);
}
/**
* Consume the result of the JSONPath evaluation and provide a parameterized type.
* @since 6.2
*/
public <T> WebTestClient.BodyContentSpec value(ParameterizedTypeReference<T> targetType, Consumer<T> consumer) {
T value = this.pathHelper.evaluateJsonPath(this.content, targetType);
consumer.accept(value);
return this.bodySpec;
}
@Override
public boolean equals(@Nullable Object obj) {
throw new AssertionError("Object#equals is disabled " +

View File

@ -1035,6 +1035,15 @@ public interface WebTestClient {
*/
BodyContentSpec xml(String expectedXml);
/**
* Access to response body assertions using a
* <a href="https://github.com/jayway/JsonPath">JsonPath</a> expression
* to inspect a specific subset of the body.
* @param expression the JsonPath expression
* @since 6.2
*/
JsonPathAssertions jsonPath(String expression);
/**
* Access to response body assertions using a
* <a href="https://github.com/jayway/JsonPath">JsonPath</a> expression
@ -1043,7 +1052,9 @@ public interface WebTestClient {
* formatting specifiers as defined in {@link String#format}.
* @param expression the JsonPath expression
* @param args arguments to parameterize the expression
* @deprecated in favor of calling {@link String#formatted(Object...)} upfront
*/
@Deprecated(since = "6.2", forRemoval = true)
JsonPathAssertions jsonPath(String expression, Object... args);
/**

View File

@ -0,0 +1,65 @@
/*
* 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.web.reactive.server;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.TypeRef;
import org.junit.jupiter.api.Test;
import org.springframework.http.codec.json.Jackson2JsonDecoder;
import org.springframework.http.codec.json.Jackson2JsonEncoder;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link EncoderDecoderMappingProvider}.
*
* @author Stephane Nicoll
*/
class EncoderDecoderMappingProviderTests {
private static final ObjectMapper objectMapper = new ObjectMapper();
private final EncoderDecoderMappingProvider mappingProvider = new EncoderDecoderMappingProvider(
new Jackson2JsonEncoder(objectMapper), new Jackson2JsonDecoder(objectMapper));
@Test
void mapType() {
Data data = this.mappingProvider.map(jsonData("test", 42), Data.class, Configuration.defaultConfiguration());
assertThat(data).isEqualTo(new Data("test", 42));
}
@Test
void mapGenericType() {
List<?> jsonData = List.of(jsonData("first", 1), jsonData("second", 2), jsonData("third", 3));
List<Data> data = this.mappingProvider.map(jsonData, new TypeRef<List<Data>>() {}, Configuration.defaultConfiguration());
assertThat(data).containsExactly(new Data("first", 1), new Data("second", 2), new Data("third", 3));
}
private Map<String, Object> jsonData(String name, int counter) {
return Map.of("name", name, "counter", counter);
}
record Data(String name, int counter) {}
}

View File

@ -0,0 +1,80 @@
/*
* 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.web.reactive.server;
import java.util.List;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.http.codec.DecoderHttpMessageReader;
import org.springframework.http.codec.EncoderHttpMessageWriter;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.http.codec.ResourceHttpMessageReader;
import org.springframework.http.codec.ResourceHttpMessageWriter;
import org.springframework.http.codec.json.Jackson2JsonDecoder;
import org.springframework.http.codec.json.Jackson2JsonEncoder;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link JsonEncoderDecoder}.
*
* @author Stephane Nicoll
*/
class JsonEncoderDecoderTests {
private static final ObjectMapper objectMapper = new ObjectMapper();
private static final HttpMessageWriter<?> jacksonMessageWriter = new EncoderHttpMessageWriter<>(
new Jackson2JsonEncoder(objectMapper));
private static final HttpMessageReader<?> jacksonMessageReader = new DecoderHttpMessageReader<>(
new Jackson2JsonDecoder(objectMapper));
@Test
void fromWithEmptyWriters() {
assertThat(JsonEncoderDecoder.from(List.of(), List.of(jacksonMessageReader))).isNull();
}
@Test
void fromWithEmptyReaders() {
assertThat(JsonEncoderDecoder.from(List.of(jacksonMessageWriter), List.of())).isNull();
}
@Test
void fromWithSuitableWriterAndNoReader() {
assertThat(JsonEncoderDecoder.from(List.of(jacksonMessageWriter), List.of(new ResourceHttpMessageReader()))).isNull();
}
@Test
void fromWithSuitableReaderAndNoWriter() {
assertThat(JsonEncoderDecoder.from(List.of(new ResourceHttpMessageWriter()), List.of(jacksonMessageReader))).isNull();
}
@Test
void fromWithNoSuitableReaderAndWriter() {
JsonEncoderDecoder jsonEncoderDecoder = JsonEncoderDecoder.from(
List.of(new ResourceHttpMessageWriter(), jacksonMessageWriter),
List.of(new ResourceHttpMessageReader(), jacksonMessageReader));
assertThat(jsonEncoderDecoder).isNotNull();
assertThat(jsonEncoderDecoder.encoder()).isInstanceOf(Jackson2JsonEncoder.class);
assertThat(jsonEncoderDecoder.decoder()).isInstanceOf(Jackson2JsonDecoder.class);
}
}

View File

@ -16,15 +16,21 @@
package org.springframework.test.web.servlet.samples.client.standalone;
import java.util.List;
import java.util.function.Consumer;
import jakarta.validation.constraints.NotNull;
import org.junit.jupiter.api.Test;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient.BodyContentSpec;
import org.springframework.test.web.servlet.client.MockMvcWebTestClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.equalTo;
/**
@ -32,60 +38,64 @@ import static org.hamcrest.Matchers.equalTo;
* {@link org.springframework.test.web.servlet.samples.standalone.ResponseBodyTests}.
*
* @author Rossen Stoyanchev
* @author Stephane Nicoll
*/
class ResponseBodyTests {
@Test
void json() {
MockMvcWebTestClient.bindToController(new PersonController()).build()
execute("/persons/Lee", body -> body.jsonPath("$.name").isEqualTo("Lee")
.jsonPath("$.age").isEqualTo(42)
.jsonPath("$.age").value(equalTo(42))
.jsonPath("$.age").value(Float.class, equalTo(42.0f)));
}
@Test
void jsonPathWithCustomType() {
execute("/persons/Lee", body -> body.jsonPath("$").isEqualTo(new Person("Lee", 42)));
}
@Test
void jsonPathWithResolvedValue() {
execute("/persons/Lee", body -> body.jsonPath("$").value(Person.class,
candidate -> assertThat(candidate).isEqualTo(new Person("Lee", 42))));
}
@Test
void jsonPathWithResolvedGenericValue() {
execute("/persons", body -> body.jsonPath("$").value(new ParameterizedTypeReference<List<Person>>() {},
candidate -> assertThat(candidate).hasSize(3).extracting(Person::name)
.containsExactly("Rossen", "Juergen", "Arjen")));
}
private void execute(String uri, Consumer<BodyContentSpec> assertions) {
assertions.accept(MockMvcWebTestClient.bindToController(new PersonController()).build()
.get()
.uri("/person/Lee")
.uri(uri)
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBody()
.jsonPath("$.name").isEqualTo("Lee")
.jsonPath("$.age").isEqualTo(42)
.jsonPath("$.age").value(equalTo(42))
.jsonPath("$.age").value(Float.class, equalTo(42.0f));
.expectBody());
}
@RestController
@SuppressWarnings("unused")
private static class PersonController {
@GetMapping("/person/{name}")
@GetMapping("/persons")
List<Person> getAll() {
return List.of(new Person("Rossen", 42), new Person("Juergen", 42),
new Person("Arjen", 42));
}
@GetMapping("/persons/{name}")
Person get(@PathVariable String name) {
Person person = new Person(name);
person.setAge(42);
return person;
return new Person(name, 42);
}
}
@SuppressWarnings("unused")
private static class Person {
@NotNull
private final String name;
private int age;
public Person(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public int getAge() {
return this.age;
}
public void setAge(int age) {
this.age = age;
}
}
private record Person(@NotNull String name, int age) {}
}

View File

@ -64,12 +64,12 @@ public class JsonPathAssertionTests {
client.get().uri("/music/people")
.exchange()
.expectBody()
.jsonPath(composerByName, "Johann Sebastian Bach").exists()
.jsonPath(composerByName, "Johannes Brahms").exists()
.jsonPath(composerByName, "Edvard Grieg").exists()
.jsonPath(composerByName, "Robert Schumann").exists()
.jsonPath(performerByName, "Vladimir Ashkenazy").exists()
.jsonPath(performerByName, "Yehudi Menuhin").exists()
.jsonPath(composerByName.formatted("Johann Sebastian Bach")).exists()
.jsonPath(composerByName.formatted("Johannes Brahms")).exists()
.jsonPath(composerByName.formatted("Edvard Grieg")).exists()
.jsonPath(composerByName.formatted("Robert Schumann")).exists()
.jsonPath(performerByName.formatted("Vladimir Ashkenazy")).exists()
.jsonPath(performerByName.formatted("Yehudi Menuhin")).exists()
.jsonPath("$.composers[0]").exists()
.jsonPath("$.composers[1]").exists()
.jsonPath("$.composers[2]").exists()
@ -117,16 +117,13 @@ public class JsonPathAssertionTests {
@Test
public void hamcrestMatcherWithParameterizedJsonPath() {
String composerName = "$.composers[%s].name";
String performerName = "$.performers[%s].name";
client.get().uri("/music/people")
.exchange()
.expectBody()
.jsonPath(composerName, 0).value(startsWith("Johann"))
.jsonPath(performerName, 0).value(endsWith("Ashkenazy"))
.jsonPath(performerName, 1).value(containsString("di Me"))
.jsonPath(composerName, 1).value(is(in(Arrays.asList("Johann Sebastian Bach", "Johannes Brahms"))));
.jsonPath("$.composers[0].name").value(startsWith("Johann"))
.jsonPath("$.performers[0].name").value(endsWith("Ashkenazy"))
.jsonPath("$.performers[1].name").value(containsString("di Me"))
.jsonPath("$.composers[1].name").value(is(in(Arrays.asList("Johann Sebastian Bach", "Johannes Brahms"))));
}