Skip to content

7.3.0 Release Notes

Migration guide

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 like target("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. Use target("/") 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") (but client.get("/some/path", Void.class) will also work).
  • basePathMain() and targetMain() methods now deprecated: use basePathApp() and targetApp() 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 with request.configurePath(target -> target.path("foo"))
  • Invocation.Builder - with request.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 - use targetApp instead
  • basePathMain - use basePathApp instead

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.