8.0.0 Release Notes¶
- Dropwizard 5 (requires java 17)
- Guice without bundled ASM
- Application fields injection
-
Tests support:
- Apache test client
- Unify ClientSupport and stubs rest
- Test client fields support
- Application urls builder
- Fix stubs rest early initialization
Guice without bundled ASM¶
Guicey now use guice with a classes classifier without bundled ASM. ASM is provided as a direct dependency.
Required to support recent java versions (>22).
Application fields injection¶
It is now possible to use injections in the application class. Useful for accessing services in the application run method:
public class App extends Application<Configuration> {
@Inject Service service;
@Override
public void initialize(Bootstrap<Configuration> bootstrap) {
bootstrap.addBundle(GuiceBundle.builder().build());
}
@Override
public void run(Configuration configuration, Environment environment) throws Exception {
service.doSomething();
}
}
Apache test client¶
DefaultTestClientFactory class was reworked to support extensibility.
ApacheTestClientFactory added (extending default factory) to use apache client (Apache5ConnectorProvider)
instead of urlconnection (HttpUrlConnectorProvider).
Why apache client is not set as default
Apache client has problems with multipart requests (not a bug, but technical limitation).
Urlconnection client has problems with PATCH requests on java > 16 (requires additional --add-opens).
Urlconnection remains as default because PATCH requests are less usable than multipart forms.
There are new shortcuts to switch to apache client in test:
@TestGuiceyApp(value=App.class, apacheClient=true)
Also, ClientSupport class now provides a shortcut to quickly switch client type
withing a test:
public void testSomething(ClientSupport client) {
ClientSupport apacheClient = client.apacheClient();
ClientSupport urlConnectionClient = client.urlConnectionClient();
}
Unify ClientSupport and stubs rest¶
Guicey provides default (universal) test client ClientSupport (used for integration tests) and
stubs rest (@StubRest) RestClient (used for lightweight rest tests).
Now base client methods are unified by the TestClient class (extended by both):
this means both clients provide exactly the same request-related methods.
Also, as ClientSupport represents an application root client, which is not
very useful in tests, it now provides 3 additinoal clients:
.appClient()- application context client (could not be the same as application root).adminClient()- admin context client.restClient()- rest context client
Plus, there is an ability to create a client for an external api: .externalClient("http://api.com")
With it, you can use the same client request methods for different contexts (different base urls). For example:
public void testSomething(ClientSupport client) {
User user = client.restClient().get("/users/123", User.class);
}
Client API changes¶
The main change is in ClientSupport.target() methods, which were previously
supporting a sequence of strings: target(String... pathParts), but now
String.format() is used: target(String format, Object... args).
!!!! note "Why String.format?"
Sequence of paths appears to be a bad idea. Compare:
target("a", "12", "c") and target("a/%s/c", 12).
The latter is more readable and more flexible.
Moreover, all shortcut methods now support String.format() syntax too:
get("/users/%s", 123) and post("/users/%s", new User(), 123)
There is now not only Class-based shortcuts, but also a GenericType-based:
Uset user = client.get("/users/%s", User.class, 123);
List<User> list = client.get("/users", new GenericType<>() {});
As an addition to existing method shortcuts (get(), post(), etc),
there is a new patch() shortcut: client.patch("/users/%s", new User(), 123)
Breaking changes
- Due to
String.format()-based syntax, old calls liketarget("a", "12", "c")will work incorrectly now. - It was possible to use target method without arguments to get root path:
target(), but now it is not possible. Usetarget("/")instead. - Old calls like
client.get("/some/path", null)used for void calls will also not work (jvm will not know what method to choose: Class or GenericType). Instead, there is a special void shortcut now:client.get("/some/path")(butclient.get("/some/path", Void.class)will also work). basePathMain()andtargetMain()methods now deprecated: usebasePathApp()andtargetApp()instead.
Default logger change¶
By default, all ClientSupport (and stubs RestClient) requests are logged,
but before it was not logging multipart requests.
Now the logger is configured to log everything by default, including multipart requests.
Request defaults¶
The initial concept of request defaults was introduced in stubs rest RestClient.
Now it evolved to be a universal concept for all clients.
Each TestClient provide "default*" methods to set request defaults:
defaultHeader("Name", "value")defaultQueryParam("Name", "value")defaultCookie("Name", "value")defaultAccept("application/json")- etc.
The most obvious use case is authorization:
public void testSomething(ClientSupport client) {
client.defaultHeader("Authorization", "Bearer 123");
User user = client.restClient().get("/users/123", User.class);
}
Sub clients¶
There is a concept of sub clients. It is used to create a client for a specific sub-url.
For example, suppose all called methods in test have some base path: /{somehting}/path/to/resource.
Instead of putting it into each request:
public void testSomething(ClientSupport client) {
TestClient rest = client.restClient();
rest.get("/%s/path/to/resource/%s", User.class, "path", 12);
rest.post("/%s/path/to/resource/%s", new User(...), "path", 12);
}
A sub client can be created:
public void testSomething(ClientSupport client) {
TestClient rest = client.restClient().subClient("/{something}/path/to/resource")
.defaultPathParam("something", "path");
rest.get("/%s", User.class, "path", 12);
rest.post("/%s", new User(...), "path", 12);
}
Note
Sub clients inherit defaults of parent client.
client.defaultQueryParam("q", "v");
TestClient rest = client.subClient("/path/to/resource");
// inherited query parameter q=v will be applied to all requests
rest.get("/%s", User.class, 12);
Defaults could be cleared at any time with client.reset().
There is a special sub client creation method using jersey UriBuilder, required
to properly support matrix parameters in the middle of the path:
TestClient sub = client.subClient(builder -> builder.path("/some/path").matrixParam("p", 1));
// /some/path;p=1/users/12
sub.get("/%s", User.class, 12);
New builder API¶
There is a new builder API for TestClient covering all possible configurations
for jersey WebTarget and Invocation.Builder. The main idea was to simplify
request configuration: to provide all possible methods in one place.
For example:
client.buildGet("/path")
.queryParam("q", "v")
.as(User.class)
Request specific extensions and properties are also supported:
client.buildGet("/path")
.queryParam("q", "v")
.register(VoidBodyReader.class)
.property(ClientProperties.FOLLOW_REDIRECTS, Boolean.FALSE)
.asVoid();
All builder methods start with a "build" prefix (buildGet(), buildPost() or generic build()).
Builder provides direct value mappings:
.as(Class).as(GenericType).asVoid().asString()
And methods, returning raw (wrapped) response:
.invoke()- response without status checks.expectSuccess()- fail if not success.expectSuccess(201, 204)- fail if not success or not expected status.expectRedirect()- fail if not redirect (method also disabled redirects following).expectRedirect(301)- fail if not redirect or not expected status.expectFailure()- fail if success.expectFailure(400)- fail success or not expected status
Response wrapper would be described below.
Debugging¶
Considering the client defaults inheritance (potential decentralized request configuration) it might be unobvious what was applied to the request.
Request builder provides a debug() option, which will print all applied defaults
and direct builder configurations to the console:
client.buildGet("/path")
.queryParam("q", "v")
.debug()
.as(User.class)
Request configuration:
Path params:
p1=1 at r.v.d.g.t.c.builder.(RequestBuilderTest.java:61)
p2=2 at r.v.d.g.t.c.builder.(RequestBuilderTest.java:62)
p3=3 at r.v.d.g.t.c.builder.(RequestBuilderTest.java:62)
Query params:
q1=1 at r.v.d.g.t.c.builder.(RequestBuilderTest.java:57)
q2=2 at r.v.d.g.t.c.builder.(RequestBuilderTest.java:58)
q3=3 at r.v.d.g.t.c.builder.(RequestBuilderTest.java:58)
Accept:
application/json at r.v.d.g.t.c.builder.(RequestBuilderTest.java:54)
Jersey request configuration:
Resolve template at r.v.d.g.t.c.builder.(TestRequestConfig.java:869)
(encodeSlashInPath=false encoded=true)
p1=1
p2=2
p3=3
Query param at r.v.d.g.t.c.b.u.conf.(JerseyRequestConfigurer.java:82)
q1=1
Query param at r.v.d.g.t.c.b.u.conf.(JerseyRequestConfigurer.java:82)
q2=2
Query param at r.v.d.g.t.c.b.u.conf.(JerseyRequestConfigurer.java:82)
q3=3
Accept at r.v.d.g.t.c.builder.(TestRequestConfig.java:899)
[application/json]
It shows two blocks:
- How request builder was configured (including defaults source)
- How jersey request was configured
The latter is obtained by wrapping jersey WebTarget and Invocation.Builder
objects to intercept all calls.
Debug could be enabled for all requests: client.defaultDebug(true).
Request assertions¶
It would not be very useful for the majority of cases, but as debug api could aggregate all request configuration data, it is possible to assert on it:
client.buildGet("/some/path")
.matrixParam("p1", "1")
.assertRequest(tracker -> assertThat(tracker.getUrl()).endsWith("/some/path;p1=1"))
.as(SomeEntity.class);
or
.assertRequest(tracker -> assertThat(tracker.getQueryParams().get("q")).isEqualTo("1"))
Form builder¶
There is a special builder helping build urlencoded and multipart requests (forms):
// urlencoded
client.buildForm("/some/path")
.param("name", 1)
.param("date", 2)
.buildPost()
.as(String.class);
// multipart
client.buildForm("/some/path")
.param("foo", "bar")
.param("file", new File("src/test/resources/test.txt"))
.buildPost()
.asVoid();
Also, it could be used to simply create a request entity and use it directly:
Entity entity = client.buildForm(null)
.param("foo", "bar")
.param("file", new File("src/test/resources/test.txt"))
.buildEntity()
client.post("/some/path", entity);
Builder will serialize all provided (non-multipart) parameters to string. For dates, it is possible to specify a custom date format:
client.buildForm("/some/path")
.dateFormat("dd/MM/yyyy")
.param("date", new Date())
.param("date2", LocalDate.now())
.buildPost()
.asVoid();
(java.util and java.time date formatters could be set separately with dateFormatter() or dateTimeFormatter() methods)
The default format could be changed globally: client.defaultFormDateFormat("dd/MM/yyyy")
(or defaultFormDateFormatter() with defaultFormDateTimeFormatter()).
Response assertions¶
As mentioned above, request builder method like .invoke() or .expectSuccess() returns
a special response wrapper object. It provides a lot of useful assertions to simplify
response data testing (avoid boilerplate code).
For example, check a response header, cookie and obtain value
User user = rest.buildGet("/users/123")
.expectSuccess()
.assertHeader("Token" , s -> s.startsWith("My-Header;"))
.assertCookie("MyCookie", "12")
.as(User.class);
Here assertion error will be thrown if header or cookie was not provided or condition does not match.
Redirection correctness could be checked as:
@Path("/resources")
public class Resource {
@Inject
AppUrlBuilder urlBuilder;
@Path("/list")
@GET
public Response get() {
...
}
@Path("/redirect")
@GET
public Response redirect() {
return Response.seeOther(
urlBuilder.rest(SuccFailRedirectResource.class).method(Resource::get).buildUri()
).build();
}
}
rest.method(Resource::redirect)
// throw error if not 3xx; also, this disables redirects following
.expectRedirect()
.assertHeader("Location", s -> s.endsWith("/resources/list"));
Also, "with*" methods could be used for completely manual assertions:
rest.method(Resource::redirect)
.expectSuccess(201)
.withHeader("MyHeader", s ->
assertThat(s).startsWith("My-Header;"));
Response object could be converted without additional variables:
String value = rest.method(Resource::redirect)
.expectSuccess()
.as(res -> res.readEntity(SomeClass.class).getProperty());
Jersey API¶
As before, it is possible to use client.target("/path") to build raw jersey target
(with the correct base path). But without applied defaults.
Direct Invocation.Builder could be built with client.request("/path").
Here all defaults would be applied.
Builders does not hide native jersey API:
WebTarget- could be modified directly withrequest.configurePath(target -> target.path("foo"))Invocation.Builder- withrequest.configureRequest(req -> req.header("foo", "bar"))
Such modifiers could be applied as client defaults:
client.defaultPathConfiguration(...)client.defaultRequestConfiguration(...)
Response wrapper also provides direct access to jersey Response object:
response.asResponse().
Resource clients¶
There is a special type of type-safe clients based on the simple idea: resource class declaration already provides all required metadata to configure a test request:
@Path("/users")
public class UserResource {
@Path("/{id}")
@GET
public User get(@NotNull @PathParam("id") Integer id) {}
}
Resource declares its path in the root @Path annotation and method annotations
tell that it's a GET request on path /users/{id} with required path parameter.
public void testSomething(ClientSupport client) {
// essentially, it's a sub client build with the resource path (from @Path annotation)
ResourceClient<UserResource> rest = client.restClient(UserResource.class);
User user = rest.method(r -> r.get(123)).as(User.class);
}
By using a mock object call (r -> r.get(123)) we specify a source of metadata and the required values
for request. Using it, a request builder is configured automatically.
It is not required to use all parameters (reverse mapping is not always possible): use null for not important arguments. All additional configurations could be done manually:
public void testSomething(ClientSupport client) {
ResourceClient<UserResource> rest = client.restClient(UserResource.class);
User user = rest.method(r -> r.get(null))
.pathParam("id", 123)
.as(User.class);
}
Almost everything could be recognized:
- All parameter annotations like
@QueryParam,@PathParam,@HeaderParam,@MatrixParam,@FormParam, etc. - All request methods:
GET,POST,PUT,DELETE,PATCH. - Request body mapping:
void post(MyEntity entity) - And even multipart forms
Not related arguments should be simply ignored:
public void get(@PathParam("id") Integer id, @Context HttpServletRequest request) {}
rest.method(r -> r.get(123, null));
Note
ResourceClient extends TestClient, so all usual method shortucts are also available for resource client
(real method calls usage is not mandatory).
Multipart forms¶
Multipart resource methods often use special multipart-related entities, like:
@Path("/multipart")
@POST
@Consumes(MediaType.MULTIPART_FORM_DATA)
public String multipart(
@NotNull @FormDataParam("file") InputStream uploadedInputStream,
@NotNull @FormDataParam("file") FormDataContentDisposition fileDetail)
Which is not handy to create manually. To address this, ResourceClient provides a
special helper object to build multipart-related values:
rest.multipartMethod((r, multipart) ->
r.multipart(multipart.fromClasspath("/sample.txt"),
multipart.disposition("file", "sample.txt"))
.asVoid());
Here file stream passed as a first parameter and filename with the second one.
Or
@Path("/multipart2")
@POST
@Consumes(MediaType.MULTIPART_FORM_DATA)
public String multipart2(
@NotNull @FormDataParam("file") FormDataBodyPart file)
rest.multipartMethod((r, multipart) ->
r.multipart2(multipart.streamPart("file", "/sample.txt")))
.asVoid();
In case of generic multipart object argument:
@Path("/multipartGeneric")
@POST
@Consumes(MediaType.MULTIPART_FORM_DATA)
public String multipartGeneric(@NotNull FormDataMultiPart multiPart)
there is a special builder:
rest.multipartMethod((r, multipart) ->
r.multipartGeneric(multipart.multipart()
.field("foo", "bar")
.stream("file", "/sample.txt")
.build()))
.as(String.class);
Note
Multipart methods require the urlencoded client (default) and, most likely, will fail with the apache client.
Sub resources¶
When a sub resource is declared with an instance:
public class Resource {
@Path("/sub")
public SubResource sub() {
return new SubResource();
}
}
it could be easily called directly:
User user = rest.method(r -> r.sub().get(123)).as(User.class);
When a sub resource method is using class:
public class Resource {
@Path("/sub")
public Class<SubResource> sub() {
return SubResource.class;
}
}
you'll have to build a sub-client first:
ResourceClient<SubResource> subRest = rest.subResource(Resource::sub, SubResource.class);
Important
Jersey ignores sub-resource @Path annotation, so special method for sub resource clients is required.
Resource typification¶
It is not always possible to use resource class to buld a sub client
(with .restClient(Resource.class)).
In such cases you can build a resource path manually and then "cast" client to the resource type:
ResourceClient<MyResource> rest = client.subClient("/resource/path")
.asResourceClient(MyResource.class);
or just buid path manually:
ResourceClient<MyResource> rest = client.subClient(
builder -> builder.path("/resource").matrixParam("p", 123),
MyResource.class);
Test client fields support¶
Before, ClientSupport could only be injected as test method (or setup method) parameter.
parameter:
public void testSomething(ClientSupport client)
Now it is possible to inject it as a field:
@WebClient
ClientSupport client;
It is also possible to inject its sub clients:
@WebClient(WebClientType.App)
TestClient app;
@WebClient(WebClientType.Admin)
TestClient admin;
@WebClient(WebClientType.Rest)
TestClient rest;
Additionally, ResourceClient could be injected directly:
@WebResourceClient
ResourceClient<MyResource> rest;
Important
Resource client injection works both with integration tests (real client) and stub rest (lightweight tests):
@TestGuiceyApp(MyApp.class)
public class MyTest {
StubRest
RestClient client;
@WebResourceClient
ResourceClient<MyResource> rest;
}
Note that resource client could be directly obtained form RestClient:
client.restClient(MyResource.class) (same as in ClientSupport).
Important
Clients assigned with a field would reset client defaults automatically (call client.reset()) after
each test method. This could be disabled with @WebClient(autoReset = false).
Application urls builder¶
Mechanism, used in resource clients, could be also used to build application urls in a type-safe manner.
A new class AppUrlBuilder added to support this. It is not bound by default
in the guice context, but could be injected (as jit binding):
@Inject AppUrlBuilder builder;
Or it could be created manually: new AppUrlBuilder(environment)
There are 3 scenarios:
- Localhost urls: the default mode when all urls contain "localhost" and application port.
- Custom host:
builder.forHost("myhost.com")when custom host used instead of localhost and application port is applied automatically - Proxy mode:
builder.forProxy("https://myhost.com")when application is behind some proxy (like apache or nginx) hiding its real port.
Examples:
// http://localhost:8080/
builder.root("/")
// http://localhost:8080/
builder.app("/")
// http://localhost:8081/
builder.admin("/")
// http://localhost:8080/
builder.rest("/")
// http://localhost:8080/users/123
builde.rest(Resource.class).method(r -> r.get(123)).build()
// http://localhost:8080/users/123
builde.rest(Resource.class).method(r -> r.get(null)).pathParam("id", 123).build()
// https://some.com:8081/something
builder.forHost("https://some.com").admin("/something")
// https://some.com/something
builder.forProxy("https://some.com").admin("/something")
Before, application server configuration detection logic was only implemented inside ClientSupport,
now it was ported to AppUrlBuilder, which you can use to obtain:
// 8080
builder.getAppPort();
// 8081
builder.getAdminPort();
// "/" (server.adminContextPath)
builder.getAdminContextPath();
// "/" (server.applicationContextPath)
builder.getAppContextPath();
// "/" (server.rootPath)
builder.getRestContextPath();
Fix stubs rest early initialization¶
@StubRest rest context was starting too early, causing closed configuration error
when Application#run method tried to configure it (environment.jersey().register(Something.class)).
Now it is started after the application run.
Auto close object stored in the shared state¶
SharedConfigurationState values, implementing AutoCloseable could be closed
automatically now after application shutdown.
Migration guide¶
All breaking changes affect only test code (ClientSupport and RestClient).
Target methods¶
Before:
public void testSomething(ClientSupport client) {
// use String... method
client.targetMain("/path/", 12, "part");
}
Now:
// use String.format syntax
client.targetApp("/path/%s/part", 12);
Deprecations:
targetMain- usetargetAppinsteadbasePathMain- usebasePathAppinstead
Shortcuts¶
Before, to receive a void response, you had to use:
client.post("/post", entity, null);
Now, because there are two variations of each shortcut method (with Class and GenericType), java would not know which one to use:
// either use new shortcut
client.post("/post", entity);
// or use Void.class directly
client.post("/post", entity, Void.class);
Default status¶
Before, RestClient (stubs rest) provide a default status method client.defaultOk
to specify what statuses to treat as success:
@StubRest
RestClient client;
public void testSomething() {
rest.defaultOk(201);
// error on 200 response (only 201 required)
rest.post("/some/path", entity, null);
}
The same could be achieved now with a new builder:
rest.buildPost("/some/path", entity)
.expectSuccess(201);
Note that, by default, a jersey client assumes response as success if its status is 2xx (by status family). This approach is more logical. Limitation to only some 2xx statuses is rarely needed.