Testing rest¶
Guicey provides lightweight REST testing support: same as dropwizard resource testing support, but with guicey-specific features.
Such tests would not start web container: all rest calls are simulated (but still, it tests every part of resource execution).
Important
Rest stubs work only with lightweight guicey run (they are simply useless when web container started)
Lightweight REST could be declared with @StubRest
annotation.
@TestGuiceyApp(App.class)
public class Test {
@StubRest
RestClient rest;
@Test
public void test() {
String res = rest.get("/foo", String.class);
Assertions.assertEquals("something", res);
WebApplicationException ex = Assertions.assertThrows(WebApplicationException.class,
() -> rest.get("/error", String.class));
Assertions.assertEquals("error message", ex.getResponse().readEntity(String.class));
}
}
Note
Extension naming is not quite correct: it is not a stub, but real application resources are used. The word "stub" used to highlight the fact of incomplete startup: only rest without web.
By default, all declared resources would be started with all existing jersey extensions (filters, exception mappers, etc.). Servlets and http filters are not started (guicey disables all web extensions to avoid their (confusing) appearance in console)
Selecting resources¶
Real tests usually require just one resource (to be tested):
@StubRest(MyResource.class)
RestClient rest;
This way only one resource would be started (and all resources directly registered in application, not as guicey extension). All jersey extensions will remain.
Or a couple of resources:
@StubRest({MyResource.class, MyResource2.class})
RestClient rest;
Or you may disable some resources:
@StubRest(disableResources = {MyResource2.class, MyResource3.class})
RestClient rest;
Disabling jersey extensions¶
Often jersey extensions, required for the final application, make complications for testing.
For example, exception mapper: dropwizard register default exception mapper which returns only the error message, instead of actual exception (and so sometimes we can't check the real cause).
disableDropwizardExceptionMappers = true
disables extensions, registered by dropwizard.
When default exception mapper enabled, resource throwing runtime error would return just error code:
@Path("/some/")
@Produces("application/json")
public class ErrorResource {
@GET
@Path("/error")
public String get() {
throw new IllegalStateException("error");
}
}
@TestGuiceyApp
public class Test {
@StubRest
RestClient rest;
public void test() {
WebApplicationException ex = Assertions.assertThrows(WebApplicationException.class,
() -> rest.get("/some/error", String.class));
// exception hidden, only generic error code
Assertions.assertTrue(ex.getResponse().readEntity(String.class)
.startsWith("{\"code\":500,\"message\":\"There was an error processing your request. It has been logged"));
}
}
Without dropwizard exception mapper, we can verify exact exception:
public class Test {
@StubRest(disableDropwizardExceptionMappers = true)
RestClient rest;
public void test() {
ProcessingException ex = Assertions.assertThrows(ProcessingException.class,
() -> rest.get("/error", String.class));
// exception available
Assertions.assertTrue(ex.getCause() instanceof IllegalStateException);
}
}
It might be useful to disable application extensions also with disableAllJerseyExtensions
:
```java
@StubRest(disableDropwizardExceptionMappers = true,
disableAllJerseyExtensions = true)
RestClient rest;
This way raw resource would be called without any additional logic.
Note
Only extensions, managed by guicey could be disabled: extensions directly registered in dropwizard would remain.
Also, you can select exact extensions to use (e.g., to test it):
@StubRest(jerseyExtensions = CustomExceptionMapper.class)
RestClient rest;
Or disable only some extensions (for example, disabling extension implementing security):
@StubRest(disableJerseyExtensions = CustomSecurityFilter.class)
RestClient rest;
Debug¶
Use debug output to see what extensions were actually included and what disabled:
@TestGuiceyApp(.., debug = true)
public class Test {
@StubRest(disableDropwizardExceptionMappers = true,
disableResources = Resource2.class,
disableJerseyExtensions = RestFilter2.class)
RestClient rest;
}
REST stub (@StubRest) started on DebugReportTest$Test1:
Jersey test container factory: org.glassfish.jersey.test.inmemory.InMemoryTestContainerFactory
Dropwizard exception mappers: DISABLED
2 resources (disabled 1):
ErrorResource (r.v.d.g.t.j.s.r.support)
Resource1 (r.v.d.g.t.j.s.r.support)
2 jersey extensions (disabled 1):
RestExceptionMapper (r.v.d.g.t.j.s.r.support)
RestFilter1 (r.v.d.g.t.j.s.r.support)
Use .printJerseyConfig() report to see ALL registered jersey extensions (including dropwizard)
Requests logging¶
By default, rest client would log requests and responses:
@TestGuiceyApp(App.class)
public class Test {
@StubRest
RestClient rest;
@Test
public void test() {
String res = rest.get("/foo", String.class);
Assertions.assertEquals("something", res);
}
}
[Client action]---------------------------------------------{
1 * Sending client request on thread main
1 > GET http://localhost:0/foo
}----------------------------------------------------------
[Client action]---------------------------------------------{
1 * Client response received on thread main
1 < 200
1 < Content-Length: 3
1 < Content-Type: application/json
something
}----------------------------------------------------------
Logging could be disabled with logRequests
option: @StubRest(logRequests = false)
Container¶
By default, InMemoryTestContainerFactory used.
In-Memory container is not a real container. It starts Jersey application and
directly calls internal APIs to handle request created by client provided by
test framework. There is no network communication involved. This containers
does not support servlet and other container dependent features, but it is a
perfect choice for simple unit tests.
If it is not enough (in-memory container does not support all functions), then
use GrizzlyTestContainerFactory
The GrizzlyTestContainerFactory creates a container that can run as a light-weight,
plain HTTP container. Almost all Jersey tests are using Grizzly HTTP test container
factory.
To activate grizzly container add dependency (version managed by dropwizard BOM):
testImplementation 'org.glassfish.jersey.test-framework.providers:jersey-test-framework-provider-grizzly2'
By default, grizzly would be used if it's available on classpath, otherwise in-memory used. If you need to force any container type use:
// use in-memory container, even if grizly available in classpath
// (use to force more lightweight container, even if some tests require grizzly)
@StubRest(container = TestContainerPolicy.IN_MEMORY)
// throw error if grizzly container not available in classpath
// (use to avoid accidental in-memory use)
@StubRest(container = TestContainerPolicy.GRIZZLY)
Rest client¶
RestClient
is almost the same as ClientSupport, available for guicey extensions.
It is just limited only for rest (and so simpler to use).
Note
Just in case: ClientSupport
would not work with rest stubs (because web container is actually
not started and so ClientSupport
can't recognize a correct rest mapping path). Of course,
it could be used with a full URLs.
Client provides base methods with response mapping:
@StubRest
RestClient rest;
rest.get(path, Class)
rest.post(path, Object/Entity, Class)
rest.put(path, Object/Entity, Class)
rest.delete(path, Class)
To not overload default methods with parameters, additional data could be set with defaults:
rest.defaultHeader(String, String)
rest.defaultQueryParam(String, String)
rest.defaultAccept(String...)
rest.defaultOk(Integer...)
defaultOk
used for void responses (response class == null) to check correct response
status (default 200 (OK) and 204 (NO_CONTENT)).
So if we need to perform a post request with query param and custom header:
rest.defaultHeader("Secret", "unreadable")
.defaultQueryParam("foo", "bar");
OtherModel res = rest.post("/somehere", new SomeModel(), OtherModel.class);
Note
Multipart support is enabled automatically when dropwizard-forms available in classpath
FormDataMultiPart multiPart = new FormDataMultiPart();
multiPart.setMediaType(MediaType.MULTIPART_FORM_DATA_TYPE);
FileDataBodyPart fileDataBodyPart = new FileDataBodyPart("file",
file.toFile(),
MediaType.APPLICATION_OCTET_STREAM_TYPE);
multiPart.bodyPart(fileDataBodyPart);
rest.post(path, Entity.entity(multiPart, multiPart.getMediaType()), Something.class);
To clear defaults:
rest.reset()
Might be a part of call chain:
rest.reset().post(...)
When test needs to verify cookies, response headers, etc. use .request(path)
:
Response response = rest.request(path).get() // .post(), .put(), .delete();
All defaults are also applied in this case.
To avoid applying configured defaults, raw rest.target(path)...
could be used.