Skip to content

Commit 69c44de

Browse files
committed
Add support for conversion of the whole JSON document
Closes gh-33018
1 parent f3d390a commit 69c44de

File tree

2 files changed

+134
-5
lines changed

2 files changed

+134
-5
lines changed

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

+72
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,20 @@
1818

1919
import java.io.File;
2020
import java.io.InputStream;
21+
import java.lang.reflect.Type;
2122
import java.nio.charset.Charset;
23+
import java.nio.charset.StandardCharsets;
2224
import java.nio.file.Path;
2325
import java.util.function.Consumer;
2426

2527
import com.jayway.jsonpath.JsonPath;
2628
import com.jayway.jsonpath.PathNotFoundException;
29+
import org.assertj.core.api.AbstractAssert;
2730
import org.assertj.core.api.AbstractObjectAssert;
31+
import org.assertj.core.api.AssertFactory;
2832
import org.assertj.core.api.AssertProvider;
2933
import org.assertj.core.api.Assertions;
34+
import org.assertj.core.api.InstanceOfAssertFactories;
3035
import org.assertj.core.error.BasicErrorMessageFactory;
3136
import org.assertj.core.internal.Failures;
3237

@@ -35,8 +40,12 @@
3540
import org.springframework.core.io.FileSystemResource;
3641
import org.springframework.core.io.InputStreamResource;
3742
import org.springframework.core.io.Resource;
43+
import org.springframework.http.HttpHeaders;
44+
import org.springframework.http.HttpInputMessage;
45+
import org.springframework.http.MediaType;
3846
import org.springframework.http.converter.GenericHttpMessageConverter;
3947
import org.springframework.lang.Nullable;
48+
import org.springframework.mock.http.MockHttpInputMessage;
4049
import org.springframework.util.Assert;
4150

4251
/**
@@ -90,6 +99,62 @@ protected AbstractJsonContentAssert(@Nullable JsonContent actual, Class<?> selfT
9099
as("JSON content");
91100
}
92101

102+
/**
103+
* Verify that the actual value can be converted to an instance of the
104+
* given {@code target}, and produce a new {@linkplain AbstractObjectAssert
105+
* assertion} object narrowed to that type.
106+
* @param target the {@linkplain Class type} to convert the actual value to
107+
*/
108+
public <T> AbstractObjectAssert<?, T> convertTo(Class<T> target) {
109+
isNotNull();
110+
T value = convertToTargetType(target);
111+
return Assertions.assertThat(value);
112+
}
113+
114+
/**
115+
* Verify that the actual value can be converted to an instance of the type
116+
* defined by the given {@link AssertFactory} and return a new Assert narrowed
117+
* to that type.
118+
* <p>{@link InstanceOfAssertFactories} provides static factories for all the
119+
* types supported by {@link Assertions#assertThat}. Additional factories can
120+
* be created by implementing {@link AssertFactory}.
121+
* <p>Example: <pre><code class="java">
122+
* // Check that the JSON document is an array of 3 users
123+
* assertThat(json).convertTo(InstanceOfAssertFactories.list(User.class))
124+
* hasSize(3); // ListAssert of User
125+
* </code></pre>
126+
* @param assertFactory the {@link AssertFactory} to use to produce a narrowed
127+
* Assert for the type that it defines.
128+
*/
129+
public <ASSERT extends AbstractAssert<?, ?>> ASSERT convertTo(AssertFactory<?, ASSERT> assertFactory) {
130+
isNotNull();
131+
return assertFactory.createAssert(this::convertToTargetType);
132+
}
133+
134+
@SuppressWarnings("unchecked")
135+
private <T> T convertToTargetType(Type targetType) {
136+
String json = this.actual.getJson();
137+
if (this.jsonMessageConverter == null) {
138+
throw new IllegalStateException(
139+
"No JSON message converter available to convert %s".formatted(json));
140+
}
141+
try {
142+
return (T) this.jsonMessageConverter.read(targetType, getClass(), fromJson(json));
143+
}
144+
catch (Exception ex) {
145+
throw failure(new ValueProcessingFailed(json,
146+
"To convert successfully to:%n %s%nBut it failed:%n %s%n".formatted(
147+
targetType.getTypeName(), ex.getMessage())));
148+
}
149+
}
150+
151+
private HttpInputMessage fromJson(String json) {
152+
MockHttpInputMessage inputMessage = new MockHttpInputMessage(json.getBytes(StandardCharsets.UTF_8));
153+
inputMessage.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
154+
return inputMessage;
155+
}
156+
157+
93158
// JsonPath support
94159

95160
/**
@@ -525,4 +590,11 @@ private JsonPathNotExpected(String actual, String path) {
525590
}
526591
}
527592

593+
private static final class ValueProcessingFailed extends BasicErrorMessageFactory {
594+
595+
private ValueProcessingFailed(String actualToString, String errorMessage) {
596+
super("%nExpected:%n %s%n%s".formatted(actualToString, errorMessage));
597+
}
598+
}
599+
528600
}

spring-test/src/test/java/org/springframework/test/json/AbstractJsonContentAssertTests.java

+62-5
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,12 @@
2626
import java.util.function.Consumer;
2727
import java.util.stream.Stream;
2828

29+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
2930
import com.fasterxml.jackson.databind.ObjectMapper;
31+
import org.assertj.core.api.AbstractObjectAssert;
3032
import org.assertj.core.api.AssertProvider;
3133
import org.assertj.core.api.InstanceOfAssertFactories;
34+
import org.assertj.core.api.InstanceOfAssertFactory;
3235
import org.json.JSONException;
3336
import org.json.JSONObject;
3437
import org.junit.jupiter.api.Nested;
@@ -100,6 +103,56 @@ void satisfiesAllowFurtherAssertions() {
100103
});
101104
}
102105

106+
@Nested
107+
class ConversionTests {
108+
109+
@Test
110+
void convertToTargetType() {
111+
assertThat(forJson(SIMPSONS, jsonHttpMessageConverter))
112+
.convertTo(Family.class)
113+
.satisfies(family -> assertThat(family.familyMembers()).hasSize(5));
114+
}
115+
116+
@Test
117+
void convertToIncompatibleTargetTypeShouldFail() {
118+
AbstractJsonContentAssert<?> jsonAssert = assertThat(forJson(SIMPSONS, jsonHttpMessageConverter));
119+
assertThatExceptionOfType(AssertionError.class)
120+
.isThrownBy(() -> jsonAssert.convertTo(Member.class))
121+
.withMessageContainingAll("To convert successfully to:",
122+
Member.class.getName(), "But it failed:");
123+
}
124+
125+
@Test
126+
void convertUsingAssertFactory() {
127+
assertThat(forJson(SIMPSONS, jsonHttpMessageConverter))
128+
.convertTo(new FamilyAssertFactory())
129+
.hasFamilyMember("Homer");
130+
}
131+
132+
private AssertProvider<AbstractJsonContentAssert<?>> forJson(@Nullable String json,
133+
@Nullable GenericHttpMessageConverter<Object> jsonHttpMessageConverter) {
134+
135+
return () -> new TestJsonContentAssert(json, jsonHttpMessageConverter);
136+
}
137+
138+
private static class FamilyAssertFactory extends InstanceOfAssertFactory<Family, FamilyAssert> {
139+
public FamilyAssertFactory() {
140+
super(Family.class, FamilyAssert::new);
141+
}
142+
}
143+
144+
private static class FamilyAssert extends AbstractObjectAssert<FamilyAssert, Family> {
145+
public FamilyAssert(Family family) {
146+
super(family, FamilyAssert.class);
147+
}
148+
149+
public FamilyAssert hasFamilyMember(String name) {
150+
assertThat(this.actual.familyMembers).anySatisfy(m -> assertThat(m.name()).isEqualTo(name));
151+
return this.myself;
152+
}
153+
}
154+
}
155+
103156
@Nested
104157
class HasPathTests {
105158

@@ -261,14 +314,14 @@ void asMapIsEmpty() {
261314
void convertToWithoutHttpMessageConverterShouldFail() {
262315
JsonPathValueAssert path = assertThat(forJson(SIMPSONS)).extractingPath("$.familyMembers[0]");
263316
assertThatIllegalStateException()
264-
.isThrownBy(() -> path.convertTo(ExtractingPathTests.Member.class))
317+
.isThrownBy(() -> path.convertTo(Member.class))
265318
.withMessage("No JSON message converter available to convert {name=Homer}");
266319
}
267320

268321
@Test
269322
void convertToTargetType() {
270323
assertThat(forJson(SIMPSONS, jsonHttpMessageConverter))
271-
.extractingPath("$.familyMembers[0]").convertTo(ExtractingPathTests.Member.class)
324+
.extractingPath("$.familyMembers[0]").convertTo(Member.class)
272325
.satisfies(member -> assertThat(member.name).isEqualTo("Homer"));
273326
}
274327

@@ -283,7 +336,7 @@ void convertToIncompatibleTargetTypeShouldFail() {
283336
}
284337

285338
@Test
286-
void convertArrayToParameterizedType() {
339+
void convertArrayUsingAssertFactory() {
287340
assertThat(forJson(SIMPSONS, jsonHttpMessageConverter))
288341
.extractingPath("$.familyMembers")
289342
.convertTo(InstanceOfAssertFactories.list(Member.class))
@@ -336,8 +389,6 @@ void isNotEmptyForPathWithFilterNotMatching() {
336389
}
337390

338391

339-
private record Member(String name) {}
340-
341392
private record Customer(long id, String username) {}
342393

343394
private AssertProvider<AbstractJsonContentAssert<?>> forJson(@Nullable String json) {
@@ -836,6 +887,12 @@ private AssertProvider<AbstractJsonContentAssert<?>> forJson(@Nullable String js
836887
return () -> new TestJsonContentAssert(json, null);
837888
}
838889

890+
891+
record Member(String name) {}
892+
893+
@JsonIgnoreProperties(ignoreUnknown = true)
894+
record Family(List<Member> familyMembers) {}
895+
839896
private static class TestJsonContentAssert extends AbstractJsonContentAssert<TestJsonContentAssert> {
840897

841898
public TestJsonContentAssert(@Nullable String json, @Nullable GenericHttpMessageConverter<Object> jsonMessageConverter) {

0 commit comments

Comments
 (0)