You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
506 lines
11 KiB
506 lines
11 KiB
[[resttestclient]] |
|
= RestTestClient |
|
|
|
`RestTestClient` is an HTTP client designed for testing server applications. It wraps |
|
Spring's xref:integration/rest-clients.adoc#rest-restclient[`RestClient`] and uses it to perform requests, |
|
but exposes a testing facade for verifying responses. `RestTestClient` can be used to |
|
perform end-to-end HTTP tests. It can also be used to test Spring MVC |
|
applications without a running server via MockMvc. |
|
|
|
|
|
|
|
|
|
[[resttestclient-setup]] |
|
== Setup |
|
|
|
To set up a `RestTestClient` you need to choose a server setup to bind to. This can be one |
|
of several MockMvc setup choices, or a connection to a live server. |
|
|
|
|
|
|
|
[[resttestclient-controller-config]] |
|
=== Bind to Controller |
|
|
|
This setup allows you to test specific controller(s) via mock request and response objects, |
|
without a running server. |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,indent=0,subs="verbatim,quotes"] |
|
---- |
|
RestTestClient client = |
|
RestTestClient.bindToController(new TestController()).build(); |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,indent=0,subs="verbatim,quotes"] |
|
---- |
|
val client = RestTestClient.bindToController(TestController()).build() |
|
---- |
|
====== |
|
|
|
[[resttestclient-context-config]] |
|
=== Bind to `ApplicationContext` |
|
|
|
This setup allows you to load Spring configuration with Spring MVC |
|
infrastructure and controller declarations and use it to handle requests via mock request |
|
and response objects, without a running server. |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,indent=0,subs="verbatim,quotes"] |
|
---- |
|
@SpringJUnitConfig(WebConfig.class) // <1> |
|
class MyTests { |
|
|
|
RestTestClient client; |
|
|
|
@BeforeEach |
|
void setUp(ApplicationContext context) { // <2> |
|
client = RestTestClient.bindToApplicationContext(context).build(); // <3> |
|
} |
|
} |
|
---- |
|
<1> Specify the configuration to load |
|
<2> Inject the configuration |
|
<3> Create the `RestTestClient` |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,indent=0,subs="verbatim,quotes"] |
|
---- |
|
@SpringJUnitConfig(WebConfig::class) // <1> |
|
class MyTests { |
|
|
|
lateinit var client: RestTestClient |
|
|
|
@BeforeEach |
|
fun setUp(context: ApplicationContext) { // <2> |
|
client = RestTestClient.bindToApplicationContext(context).build() // <3> |
|
} |
|
} |
|
---- |
|
<1> Specify the configuration to load |
|
<2> Inject the configuration |
|
<3> Create the `RestTestClient` |
|
====== |
|
|
|
[[resttestclient-fn-config]] |
|
=== Bind to Router Function |
|
|
|
This setup allows you to test xref:web/webmvc-functional.adoc[functional endpoints] via |
|
mock request and response objects, without a running server. |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,indent=0,subs="verbatim,quotes"] |
|
---- |
|
RouterFunction<?> route = ... |
|
client = RestTestClient.bindToRouterFunction(route).build(); |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,indent=0,subs="verbatim,quotes"] |
|
---- |
|
val route: RouterFunction<*> = ... |
|
val client = RestTestClient.bindToRouterFunction(route).build() |
|
---- |
|
====== |
|
|
|
[[resttestclient-server-config]] |
|
=== Bind to Server |
|
|
|
This setup connects to a running server to perform full, end-to-end HTTP tests: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,indent=0,subs="verbatim,quotes"] |
|
---- |
|
client = RestTestClient.bindToServer().baseUrl("http://localhost:8080").build(); |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,indent=0,subs="verbatim,quotes"] |
|
---- |
|
client = RestTestClient.bindToServer().baseUrl("http://localhost:8080").build() |
|
---- |
|
====== |
|
|
|
|
|
|
|
[[resttestclient-client-config]] |
|
=== Client Config |
|
|
|
In addition to the server setup options described earlier, you can also configure client |
|
options, including base URL, default headers, client filters, and others. These options |
|
are readily available following the initial `bindTo` call, as follows: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,indent=0,subs="verbatim,quotes"] |
|
---- |
|
client = RestTestClient.bindToController(new TestController()) |
|
.baseUrl("/test") |
|
.build(); |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,indent=0,subs="verbatim,quotes"] |
|
---- |
|
client = RestTestClient.bindToController(TestController()) |
|
.baseUrl("/test") |
|
.build() |
|
---- |
|
====== |
|
|
|
|
|
|
|
|
|
[[resttestclient-tests]] |
|
== Writing Tests |
|
|
|
xref:integration/rest-clients.adoc#rest-restclient[`RestClient`] and `RestTestClient` have |
|
the same API up to the point of the call to `exchange()`. After that, `RestTestClient` |
|
provides two alternative ways to verify the response: |
|
|
|
1. xref:resttestclient-workflow[Built-in Assertions] extend the request workflow with a chain of expectations |
|
2. xref:resttestclient-assertj[AssertJ Integration] to verify the response via `assertThat()` statements |
|
|
|
|
|
|
|
[[resttestclient-workflow]] |
|
=== Built-in Assertions |
|
|
|
To use the built-in assertions, remain in the workflow after the call to `exchange()`, and |
|
use one of the expectation methods. For example: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,indent=0,subs="verbatim,quotes"] |
|
---- |
|
client.get().uri("/persons/1") |
|
.accept(MediaType.APPLICATION_JSON) |
|
.exchange() |
|
.expectStatus().isOk() |
|
.expectHeader().contentType(MediaType.APPLICATION_JSON); |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,indent=0,subs="verbatim,quotes"] |
|
---- |
|
client.get().uri("/persons/1") |
|
.accept(MediaType.APPLICATION_JSON) |
|
.exchange() |
|
.expectStatus().isOk() |
|
.expectHeader().contentType(MediaType.APPLICATION_JSON) |
|
---- |
|
====== |
|
|
|
If you would like for all expectations to be asserted even if one of them fails, you can |
|
use `expectAll(..)` instead of multiple chained `expect*(..)` calls. This feature is |
|
similar to the _soft assertions_ support in AssertJ and the `assertAll()` support in |
|
JUnit Jupiter. |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,indent=0,subs="verbatim,quotes"] |
|
---- |
|
client.get().uri("/persons/1") |
|
.accept(MediaType.APPLICATION_JSON) |
|
.exchange() |
|
.expectAll( |
|
spec -> spec.expectStatus().isOk(), |
|
spec -> spec.expectHeader().contentType(MediaType.APPLICATION_JSON) |
|
); |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,indent=0,subs="verbatim,quotes"] |
|
---- |
|
client.get().uri("/persons/1") |
|
.accept(MediaType.APPLICATION_JSON) |
|
.exchange() |
|
.expectAll( |
|
{ spec -> spec.expectStatus().isOk() }, |
|
{ spec -> spec.expectHeader().contentType(MediaType.APPLICATION_JSON) } |
|
) |
|
---- |
|
====== |
|
|
|
You can then choose to decode the response body through one of the following: |
|
|
|
* `expectBody(Class<T>)`: Decode to single object. |
|
* `expectBody()`: Decode to `byte[]` for xref:testing/resttestclient.adoc#resttestclient-json[JSON Content] or an empty body. |
|
|
|
|
|
If the built-in assertions are insufficient, you can consume the object instead and |
|
perform any other assertions: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,indent=0,subs="verbatim,quotes"] |
|
---- |
|
client.get().uri("/persons/1") |
|
.exchange() |
|
.expectStatus().isOk() |
|
.expectBody(Person.class) |
|
.consumeWith(result -> { |
|
// custom assertions (for example, AssertJ)... |
|
}); |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,indent=0,subs="verbatim,quotes"] |
|
---- |
|
client.get().uri("/persons/1") |
|
.exchange() |
|
.expectStatus().isOk() |
|
.expectBody<Person>() |
|
.consumeWith { |
|
// custom assertions (for example, AssertJ)... |
|
} |
|
---- |
|
====== |
|
|
|
Or you can exit the workflow and obtain a `EntityExchangeResult`: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,indent=0,subs="verbatim,quotes"] |
|
---- |
|
EntityExchangeResult<Person> result = client.get().uri("/persons/1") |
|
.exchange() |
|
.expectStatus().isOk() |
|
.expectBody(Person.class) |
|
.returnResult(); |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,indent=0,subs="verbatim,quotes"] |
|
---- |
|
val result = client.get().uri("/persons/1") |
|
.exchange() |
|
.expectStatus().isOk |
|
.expectBody<Person>() |
|
.returnResult() |
|
---- |
|
====== |
|
|
|
TIP: When you need to decode to a target type with generics, look for the overloaded methods |
|
that accept {spring-framework-api}/core/ParameterizedTypeReference.html[`ParameterizedTypeReference`] |
|
instead of `Class<T>`. |
|
|
|
|
|
[[resttestclient-no-content]] |
|
==== No Content |
|
|
|
If the response is not expected to have content, you can assert that as follows: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,indent=0,subs="verbatim,quotes"] |
|
---- |
|
client.post().uri("/persons") |
|
.body(person) |
|
.exchange() |
|
.expectStatus().isCreated() |
|
.expectBody().isEmpty(); |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,indent=0,subs="verbatim,quotes"] |
|
---- |
|
client.post().uri("/persons") |
|
.body(person) |
|
.exchange() |
|
.expectStatus().isCreated() |
|
.expectBody().isEmpty() |
|
---- |
|
====== |
|
|
|
If you want to ignore the response content, the following releases the content without any assertions: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,indent=0,subs="verbatim,quotes"] |
|
---- |
|
client.get().uri("/persons/123") |
|
.exchange() |
|
.expectStatus().isNotFound() |
|
.expectBody(Void.class); |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,indent=0,subs="verbatim,quotes"] |
|
---- |
|
client.get().uri("/persons/123") |
|
.exchange() |
|
.expectStatus().isNotFound |
|
.expectBody<Unit>() |
|
---- |
|
====== |
|
|
|
|
|
[[resttestclient-json]] |
|
==== JSON Content |
|
|
|
You can use `expectBody()` without a target type to perform assertions on the raw |
|
content rather than through higher level Object(s). |
|
|
|
To verify the full JSON content with https://jsonassert.skyscreamer.org[JSONAssert]: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,indent=0,subs="verbatim,quotes"] |
|
---- |
|
client.get().uri("/persons/1") |
|
.exchange() |
|
.expectStatus().isOk() |
|
.expectBody() |
|
.json("{\"name\":\"Jane\"}") |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,indent=0,subs="verbatim,quotes"] |
|
---- |
|
client.get().uri("/persons/1") |
|
.exchange() |
|
.expectStatus().isOk() |
|
.expectBody() |
|
.json("{\"name\":\"Jane\"}") |
|
---- |
|
====== |
|
|
|
To verify JSON content with https://github.com/jayway/JsonPath[JSONPath]: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,indent=0,subs="verbatim,quotes"] |
|
---- |
|
client.get().uri("/persons") |
|
.exchange() |
|
.expectStatus().isOk() |
|
.expectBody() |
|
.jsonPath("$[0].name").isEqualTo("Jane") |
|
.jsonPath("$[1].name").isEqualTo("Jason"); |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,indent=0,subs="verbatim,quotes"] |
|
---- |
|
client.get().uri("/persons") |
|
.exchange() |
|
.expectStatus().isOk() |
|
.expectBody() |
|
.jsonPath("$[0].name").isEqualTo("Jane") |
|
.jsonPath("$[1].name").isEqualTo("Jason") |
|
---- |
|
====== |
|
|
|
|
|
|
|
[[resttestclient-assertj]] |
|
=== AssertJ Integration |
|
|
|
`RestTestClientResponse` is the main entry point for the AssertJ integration. |
|
It is an `AssertProvider` that wraps the `ResponseSpec` of an exchange in order to enable |
|
use of `assertThat()` statements. For example: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,indent=0,subs="verbatim,quotes"] |
|
---- |
|
ResponseSpec spec = client.get().uri("/persons").exchange(); |
|
|
|
RestTestClientResponse response = RestTestClientResponse.from(spec); |
|
assertThat(response).hasStatusOk(); |
|
assertThat(response).hasContentTypeCompatibleWith(MediaType.TEXT_PLAIN); |
|
// ... |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,indent=0,subs="verbatim,quotes"] |
|
---- |
|
val spec = client.get().uri("/persons").exchange() |
|
|
|
val response = RestTestClientResponse.from(spec) |
|
assertThat(response).hasStatusOk() |
|
assertThat(response).hasContentTypeCompatibleWith(MediaType.TEXT_PLAIN) |
|
// ... |
|
---- |
|
====== |
|
|
|
You can also use the built-in workflow first, and then obtain an `ExchangeResult` to wrap |
|
and continue with AssertJ. For example: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,indent=0,subs="verbatim,quotes"] |
|
---- |
|
ExchangeResult result = client.get().uri("/persons").exchange() |
|
. // ... |
|
.returnResult(); |
|
|
|
RestTestClientResponse response = RestTestClientResponse.from(result); |
|
assertThat(response).hasStatusOk(); |
|
assertThat(response).hasContentTypeCompatibleWith(MediaType.TEXT_PLAIN); |
|
// ... |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,indent=0,subs="verbatim,quotes"] |
|
---- |
|
val result = client.get().uri("/persons").exchange() |
|
. // ... |
|
.returnResult() |
|
|
|
val response = RestTestClientResponse.from(spec) |
|
assertThat(response).hasStatusOk() |
|
assertThat(response).hasContentTypeCompatibleWith(MediaType.TEXT_PLAIN) |
|
// ... |
|
---- |
|
======
|
|
|