A library for testing classes that use a JAX-RS Client without starting a full server or relying on mocking libraries.
Heavily inspired by Spring's Client testing infrastructure.
- JDK 17 or higher
- JAX-RS (Jakarta RESTful Web Services) 3.1 & 4.0
<dependency>
<groupId>io.github.sola-ris</groupId>
<artifactId>jax-rs-client-test</artifactId>
<version>0.2.0</version>
</dependency>The idea is to declare the expected requests and provide "mock" or "stub" responses so the code can be tested in isolation, i.e. without starting a server.
The following example shows how to do so with a Client:
Client client = ClientBuilder.newClient();
MockRestServer server = MockRestServer.bindTo(client).build();
server.expect(RequestMatchers.requestTo("/users/42"))
.andRespond(MockResponseCreators.withNotFound());
// Test code that uses the Client
server.verify();In the example, the MockRestServer (the entrypoint into the library) configures the Client with a ClientRequestFilter that matches the incoming
request against the set-up expectations and returns the "stub" responses. In this case, we expect a call to /users/42 and want to return
a response with status code 404. Additional expected requests and stub responses can be defined as needed. At the end of the test, server.verify()
can be used to verify that all the expected requests were indeed executed.
Unless specified otherwise, each expected request is expected to be executed exactly once. The expect method of the MockRestServer provides an
overload that accepts an ExpectedCount argument that specifies a range count (e.g. once, between, min, max, etc.) for the given request expectation.
The following example shows how to do so with a WebTarget:
WebTarget target = ClientBuilder.newClient().target("");
MockRestServer server = MockRestServer.bindTo(target).build();
server.expect(ExpectedCount.max(3), RequestMatchers.requestTo("/hello"))
.andRespond(MockResponseCreators.withSuccess());
server.expect(ExpectedCount.between(1, 2), RequestMatchers.requestTo("/goodbye"))
.andRespond(MockResponseCreators.withSuccess());
// Use the bound WebTarget e.g. by passing it to the class under test
server.verify();By default, only the first invocation of each expected request is expected to occur in order of declaration.
This behavior can be changed by passing the desired RequestOrder to withRequestOrder when building the server.
ClientBuilder clientBuilder = ClientBuilder.newBuilder()
MockRestServer server = MockRestServer.bindTo(clientBuilder)
.withRequestOrder(RequestOrder.UNORDERED)
.build();The following options are available:
- ORDERED
- Expect the first invocation of each request in order of declaration. Subsequent requests may occur in any order. This is the default.
- UNORDERED
- Expect all invocations in any order.
- STRICT
- Expect the minimum amount of expected requests to occur in order of declaration. Subsequent requests may occur in any order.
In some tests it may be necessary to mock only some of the requests and call an actual remote service or others.
This can be done through an ExecutingResponseCreator:
Client client = ClientBuilder.newClient();
MockRestServer server = MockRestServer.bindTo(client).build();
server.expect(RequestMatchers.requestTo("/profile/42"))
.andRespond(new ExecutingResponseCreator()); // <1>
Client customClient = ClientBuilder.newBuilder().register(new MyAuthFilter()).build(); // <2>
server.expect(RequestMatchers.requestTo("/users/42")) // <3>
.andExpect(RequestMatchers.method("GET"))
.andRespond(new ExecutingResponseCreator(customClient));
server.expect(RequestMatchers.requestTo("/users/42")) // <4>
.andExpect(RequestMatchers.method("DELETE"))
.andRespond(MockResponseCreators.withSuccess());
// Test code that uses the Client
customClient.close(); // <5>
server.verify();- Respond to
/profile/42by calling the actual service with a default Client - Create a custom Client with e.g. custom authentication
- Respond to
GET /users/42by calling the actual service throughcustomClient - Respond to
DELETE /users/42with a stub - Clients passed to an
ExecutingResponseCreatormust be closed by the caller
JAX-RS Client Test comes with a number of built-in RequestMacher implementations, all accessed via factory methods in RequestMatchers.
- RequestMatchers
- Matchers for basic request attributes like the URI or HTTP method, access to all other RequestMatcher implementations
- EntityRequestMatchers
- Matchers related to the request entity / body
- JsonPathRequestMatchers
- Matchers that evaluate a JsonPath expression against the request body
- XpathRequestMatchers
- Matchers that evaluate an XPath expression against the request body
Custom request matchers can be implemented as a lambda and can access the current ClientRequestContext.
All implementations must throw an AssertionError if the incoming request does not match and return if it does.
For example:
RequestMatcher myMatcher = request -> {
if (Locale.ENGLISH.equals(request.getLanguage())) {
throw new AssertionError("Expected a language other than ENGLISH.")
}
};When implementing a RequestMatcher, the entity can be accessed via ClientRequestContext#getEntity.
If the matcher requires a different representation of the entity, e.g. a JSON String instead of POJO, it can be converted via an EntityConverter,
which can be obtained by calling EntityConverter#fromRequestContext. The EntityConverter has the same capabilities as the client that made the
request, so if the client has a provider that can handle POJO -> JSON String conversion, the EntityConverter can do it as well.
For example:
RequestMatcher myMatcher = request -> {
EntityConverter converter = EntityConverter.fromRequestContext(request);
// Assuming the MediaType is application/json
String jsonString = converter.convertEntity(request, String.class);
// Assertions on the string
};EntityPart has certain limitations that require it to be handled separately from other request entities:
- It is
InputStreambased, meaning its contents can only be accessed once - Implementations are not required to override
equals, preventing comparison with anotherEntityPart
The EntityConverter provides two methods to work around this:
bufferExpectedMultipart, which takes an arbitraryList<EntityPart>and buffers itbufferMultipartRequest, which takes theList<EntityPart>from the currentClientRequestContext, buffers it and sets the request up for further processing or repeated buffering in anotherRequestMatcher
For example:
EntityPart myEntityPart = EntityPart.withName("username")
.mediaType(MediaType.TEXT_PLAIN_TYPE)
.content("admin")
.build();
RequestMatcher myMultipartMatcher = request -> {
EntityConverter converter = EntityConverter.fromRequestContext(request);
EntityPart myBufferedPart = converter.bufferExpectedMultipart(List.of(myEntityPart)).get(0);
// MediaType multipart/form-data and entity of type List<EntityPart> is assumed
EntityPart bufferedRequestPart = converter.bufferMultipartRequest(request).get(0);
// Assuming AssertJ is present
// Reading the content here does not prevent further reads
// in another matcher or during request execution
assertThat(myBufferedPart.getContent(String.class))
.isEqualTo(bufferedRequestPart.getContent(String.class))
// Buffered EntityParts implement equals()
assertThat(myBufferedPart)
.isEqualTo(bufferedRequestPart)
};
// Set up the MockRestServer and execute the requestBuilding JAX-RS Client Test requires JDK 25.
To build the project simply call ./mvnw clean install.
The library relies on a ClientRequestFilter that calls ClientRequestContext#abortWith in order to create its mock response, making it impossible to test classes that use a client that relies on the same behavior.