Skip to content

Commit 7ea2caa

Browse files
committed
JSON content and JsonPath support for WebTestClient
Issue: SPR-15420
1 parent 1e8c7e5 commit 7ea2caa

File tree

6 files changed

+277
-36
lines changed

6 files changed

+277
-36
lines changed

spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import org.springframework.http.MediaType;
4141
import org.springframework.http.client.reactive.ClientHttpConnector;
4242
import org.springframework.http.client.reactive.ClientHttpRequest;
43+
import org.springframework.test.util.JsonExpectationsHelper;
4344
import org.springframework.util.Assert;
4445
import org.springframework.util.MimeType;
4546
import org.springframework.util.MultiValueMap;
@@ -502,6 +503,24 @@ public EntityExchangeResult<Void> isEmpty() {
502503
return new EntityExchangeResult<>(this.result, null);
503504
}
504505

506+
@Override
507+
public BodyContentSpec json(String json) {
508+
this.result.assertWithDiagnostics(() -> {
509+
try {
510+
new JsonExpectationsHelper().assertJsonEqual(json, getBodyAsString());
511+
}
512+
catch (Exception ex) {
513+
throw new AssertionError("JSON parsing error", ex);
514+
}
515+
});
516+
return this;
517+
}
518+
519+
@Override
520+
public JsonPathAssertions jsonPath(String expression, Object... args) {
521+
return new JsonPathAssertions(this, expression, args);
522+
}
523+
505524
@Override
506525
public BodyContentSpec consumeAsStringWith(Consumer<String> consumer) {
507526
this.result.assertWithDiagnostics(() -> consumer.accept(getBodyAsString()));
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Copyright 2002-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.test.web.reactive.server;
17+
18+
import org.springframework.test.util.JsonPathExpectationsHelper;
19+
20+
21+
/**
22+
* <a href="https://github.com/jayway/JsonPath">JsonPath</a> assertions.
23+
*
24+
* @author Rossen Stoyanchev
25+
* @since 5.0
26+
* @see <a href="https://github.com/jayway/JsonPath">https://github.com/jayway/JsonPath</a>
27+
*/
28+
public class JsonPathAssertions {
29+
30+
private final WebTestClient.BodyContentSpec bodySpec;
31+
32+
private final JsonPathExpectationsHelper pathHelper;
33+
34+
35+
JsonPathAssertions(WebTestClient.BodyContentSpec spec, String expression, Object... args) {
36+
this.bodySpec = spec;
37+
this.pathHelper = new JsonPathExpectationsHelper(expression, args);
38+
}
39+
40+
41+
/**
42+
* Applies {@link JsonPathExpectationsHelper#assertValue(String, Object)}.
43+
*/
44+
public WebTestClient.BodyContentSpec isEqualTo(Object expectedValue) {
45+
this.bodySpec.consumeAsStringWith(body -> {
46+
this.pathHelper.assertValue(body, expectedValue);
47+
});
48+
return this.bodySpec;
49+
}
50+
51+
/**
52+
* Applies {@link JsonPathExpectationsHelper#exists(String)}.
53+
*/
54+
public WebTestClient.BodyContentSpec exists() {
55+
this.bodySpec.consumeAsStringWith(this.pathHelper::exists);
56+
return this.bodySpec;
57+
}
58+
59+
/**
60+
* Applies {@link JsonPathExpectationsHelper#doesNotExist(String)}.
61+
*/
62+
public WebTestClient.BodyContentSpec doesNotExist() {
63+
this.bodySpec.consumeAsStringWith(this.pathHelper::doesNotExist);
64+
return this.bodySpec;
65+
}
66+
67+
/**
68+
* Applies {@link JsonPathExpectationsHelper#assertValueIsEmpty(String)}.
69+
*/
70+
public WebTestClient.BodyContentSpec isEmpty() {
71+
this.bodySpec.consumeAsStringWith(this.pathHelper::assertValueIsEmpty);
72+
return this.bodySpec;
73+
}
74+
75+
/**
76+
* Applies {@link JsonPathExpectationsHelper#assertValueIsNotEmpty(String)}.
77+
*/
78+
public WebTestClient.BodyContentSpec isNotEmpty() {
79+
this.bodySpec.consumeAsStringWith(this.pathHelper::assertValueIsNotEmpty);
80+
return this.bodySpec;
81+
}
82+
83+
/**
84+
* Applies {@link JsonPathExpectationsHelper#assertValueIsBoolean(String)}.
85+
*/
86+
public WebTestClient.BodyContentSpec isBoolean() {
87+
this.bodySpec.consumeAsStringWith(this.pathHelper::assertValueIsBoolean);
88+
return this.bodySpec;
89+
}
90+
91+
/**
92+
* Applies {@link JsonPathExpectationsHelper#assertValueIsNumber(String)}.
93+
*/
94+
public WebTestClient.BodyContentSpec isNumber() {
95+
this.bodySpec.consumeAsStringWith(this.pathHelper::assertValueIsNumber);
96+
return this.bodySpec;
97+
}
98+
99+
/**
100+
* Applies {@link JsonPathExpectationsHelper#assertValueIsArray(String)}.
101+
*/
102+
public WebTestClient.BodyContentSpec isArray() {
103+
this.bodySpec.consumeAsStringWith(this.pathHelper::assertValueIsArray);
104+
return this.bodySpec;
105+
}
106+
107+
/**
108+
* Applies {@link JsonPathExpectationsHelper#assertValueIsMap(String)}.
109+
*/
110+
public WebTestClient.BodyContentSpec isMap() {
111+
this.bodySpec.consumeAsStringWith(this.pathHelper::assertValueIsMap);
112+
return this.bodySpec;
113+
}
114+
115+
}

spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -598,11 +598,31 @@ interface ListBodySpec<E> extends BodySpec<List<E>, ListBodySpec<E>> {
598598
interface BodyContentSpec {
599599

600600
/**
601-
* Consume the body and verify it is empty.
602-
* @return the exchange result
601+
* Assert the response body is empty and return the exchange result.
603602
*/
604603
EntityExchangeResult<Void> isEmpty();
605604

605+
/**
606+
* Parse the expected and actual response content as JSON and perform a
607+
* "lenient" comparison verifying the same attribute-value pairs.
608+
* <p>Use of this option requires the
609+
* <a href="http://jsonassert.skyscreamer.org/">JSONassert<a/> library
610+
* on to be on the classpath.
611+
* @param expectedJson the expected JSON content.
612+
*/
613+
BodyContentSpec json(String expectedJson);
614+
615+
/**
616+
* Access to response body assertions using a
617+
* <a href="https://github.com/jayway/JsonPath">JsonPath</a> expression
618+
* to inspect a specific subset of the body.
619+
* <p>The JSON path expression can be a parameterized string using
620+
* formatting specifiers as defined in {@link String#format}.
621+
* @param expression the JsonPath expression
622+
* @param args arguments to parameterize the expression
623+
*/
624+
JsonPathAssertions jsonPath(String expression, Object... args);
625+
606626
/**
607627
* Assert the response body content converted to a String with the given
608628
* {@link Consumer}. The String is created with the {@link Charset} from
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2002-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.test.web.reactive.server.samples;
17+
18+
import org.junit.Test;
19+
import reactor.core.publisher.Flux;
20+
21+
import org.springframework.http.MediaType;
22+
import org.springframework.test.web.reactive.server.WebTestClient;
23+
import org.springframework.web.bind.annotation.GetMapping;
24+
import org.springframework.web.bind.annotation.PathVariable;
25+
import org.springframework.web.bind.annotation.RestController;
26+
27+
/**
28+
* Sample tests asserting JSON response content.
29+
*
30+
* @author Rossen Stoyanchev
31+
*/
32+
public class JsonContentTests {
33+
34+
private final WebTestClient client = WebTestClient.bindToController(new PersonController()).build();
35+
36+
37+
@Test
38+
public void jsonContent() throws Exception {
39+
this.client.get().uri("/persons")
40+
.accept(MediaType.APPLICATION_JSON_UTF8)
41+
.exchange()
42+
.expectStatus().isOk()
43+
.expectBody().json("[{\"name\":\"Jane\"},{\"name\":\"Jason\"},{\"name\":\"John\"}]");
44+
}
45+
46+
@Test
47+
public void jsonPathIsEqualTo() throws Exception {
48+
this.client.get().uri("/persons")
49+
.accept(MediaType.APPLICATION_JSON_UTF8)
50+
.exchange()
51+
.expectStatus().isOk()
52+
.expectBody()
53+
.jsonPath("$[0].name").isEqualTo("Jane")
54+
.jsonPath("$[1].name").isEqualTo("Jason")
55+
.jsonPath("$[2].name").isEqualTo("John");
56+
}
57+
58+
59+
@RestController
60+
@SuppressWarnings("unused")
61+
static class PersonController {
62+
63+
@GetMapping("/persons")
64+
Flux<Person> getPersons() {
65+
return Flux.just(new Person("Jane"), new Person("Jason"), new Person("John"));
66+
}
67+
}
68+
69+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2002-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.test.web.reactive.server.samples;
17+
18+
import com.fasterxml.jackson.annotation.JsonCreator;
19+
import com.fasterxml.jackson.annotation.JsonProperty;
20+
21+
class Person {
22+
23+
private final String name;
24+
25+
@JsonCreator
26+
public Person(@JsonProperty("name") String name) {
27+
this.name = name;
28+
}
29+
30+
public String getName() {
31+
return this.name;
32+
}
33+
34+
@Override
35+
public boolean equals(Object other) {
36+
if (this == other) return true;
37+
if (other == null || getClass() != other.getClass()) return false;
38+
Person person = (Person) other;
39+
return getName().equals(person.getName());
40+
}
41+
42+
@Override
43+
public int hashCode() {
44+
return getName().hashCode();
45+
}
46+
47+
@Override
48+
public String toString() {
49+
return "Person[name='" + name + "']";
50+
}
51+
52+
}

spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ResponseEntityTests.java

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@
2222
import java.util.List;
2323
import java.util.Map;
2424

25-
import com.fasterxml.jackson.annotation.JsonCreator;
26-
import com.fasterxml.jackson.annotation.JsonProperty;
2725
import org.junit.Test;
2826
import reactor.core.publisher.Flux;
2927
import reactor.core.publisher.Mono;
@@ -169,36 +167,4 @@ ResponseEntity<String> savePerson(@RequestBody Person person) {
169167
}
170168
}
171169

172-
static class Person {
173-
174-
private final String name;
175-
176-
@JsonCreator
177-
public Person(@JsonProperty("name") String name) {
178-
this.name = name;
179-
}
180-
181-
public String getName() {
182-
return this.name;
183-
}
184-
185-
@Override
186-
public boolean equals(Object other) {
187-
if (this == other) return true;
188-
if (other == null || getClass() != other.getClass()) return false;
189-
Person person = (Person) other;
190-
return getName().equals(person.getName());
191-
}
192-
193-
@Override
194-
public int hashCode() {
195-
return getName().hashCode();
196-
}
197-
198-
@Override
199-
public String toString() {
200-
return "Person[name='" + name + "']";
201-
}
202-
}
203-
204170
}

0 commit comments

Comments
 (0)