7.1.0 Release Notes¶
- Update to dropwizard 4.0.4
- Add qualified configuration bindings
- Test improvements:
- DropwizardTestSupport and ClientSupport objects availability
- Web client improvements (ClientSupport)
- Improve generic testing
- Support commands testing
Qualified configuration bindings¶
- Any configuration property could be bound to guice just by annotating field or getter with qualifier annotation (guice or jakarta).
- Annotated fields with the same type and qualifier are bound aggregated with
Set
- Core dropwizard configuration objects could be bound with qualified overridden getter
public class MyConfig extends Configuration {
@Named("custom")
private String prop1;
@CustomQualifier
private SubObj obj1 = new SubObj();
public String getProp1() {
return prop1;
}
public SubObj getSubObj() {
return obj1;
}
@Named("metrics") // dropwizard object bind
@Override
MetricsFactory getMetricsFactory() {
return super.getMetricsFactory();
}
}
public class SubObj {
private String prop2;
private String prop3;
// aggregated binding (same type + qualifier)
@Named("sub-prop")
public String getProp2() {
return prop2;
}
@Named("sub-prop")
public String getProp3() {
return prop3;
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
@BindingAnnotation
public @interface CustomQualifier {}
The following injections would be available:
@Inject @Named("custom") String prop1;
@Inject @CustomQualifier SubObj obj1;
@Inject @Named("sub-prop") Set<String> prop23;
@Inject @Named("metrics") MetricsFactory metricsFactyry;
Note
Properties are grouped by exact type and annotation (exactly the same binding keys), so don't expect more complex grouping (for example, by some base class).
Configuration bindings report:
GuiceBundle.builder()
.printCustomConfigurationBindings()
Would show qualified bindings (with source property names in braces):
Qualified bindings:
@Named("metrics") MetricsFactory = MetricsFactory{frequency=1 minute, reporters=[], reportOnStop=false} (metrics)
@CustomQualifier SubObj = ru.vyarus.dropwizard.guice.yaml.qualifier.QualifierSampleTest$SubObj@19e0dffe (obj1)
@Named("sub-prop") Set<String> = (aggregated values)
String = "2" (obj1.prop2)
String = "3" (obj1.prop3)
@Named("custom") String = "1" (prop1)
Guice modules and guicey bundles could also access annotated values
(through DropwizardAwareModule
and GuiceyEnvironment
in GuiceyBundle#run
):
.annotatedValue(Names.named("custom"))
- access by (equal) annotation instance (for annotations with state).annotatedValue(CustomQualifier.class)
- access by annotation type
More related methods added for ConfigurationTree
object:
findAllByAnnotation
- find all annotated pathsfindByAnnotation
- find exactly one annotated path (fail if more found)annotatatedValues
- all non-null values from annotated paths
Test improvements¶
DropwizardTestSupport and ClientSupport objects availability¶
In junit 5 and spock 2 DropwizardTestSupport
object could now be injected as test parameter
(or constructor, or lifecycle method parameter):
@Test
public void testSomething(DropwizardTestSupport support) {
}
Note
GuiceyTestSupport extends DropwizardTestSupport
so this works for both web and core runs.
Also, context DropwizardTestSupport
and ClientSupport
objects now available statically (at the same thread):
DropwizardTestSupport support = TestSupport.getContext();
ClientSupport client = TestSupport.getContextClient();
These static references would also work inside generic run callbacks, like:
TestSupport.runCoreApp(App.class, injector -> {
DropwizardTestSupport support = TestSupport.getContext();
});
(works for all TestSupport.run*
methods)
Web client improvements (ClientSupport)¶
Simple methods¶
The client now contains simple GET/POST/PUT/DELETE methods for simple cases:
@Test
public void testWeb(ClientSupport client) {
// get with result
Result res = client.get("rest/sample", Result.class);
// post without result (void)
client.post("rest/action", new PostObject(), null);
}
All methods:
- Methods accept paths relative to server root. In the example above: "http://localhost:8080/rest/sample"
- Could return mapped response.
- For void calls, use null instead of the result type. In this case, only 200 and 204 (no content) responses would be considered successful
POST and PUT also accept (body) object to send. But methods does not allow multipart execution.
These methods could be used as examples for jersey client usage.
There is also a new helper method: client.basePathRoot()
returning the base server path (localhost + port);
Default client¶
JerseyClient
used inside ClientSupport
now automatically configures
multipart feature if dropwizard-forms
is in classpath (so the client could be used
for sending multipart data).
Request and response logging is enabled by default now to simplify writing (and debugging) tests. By default, all messages are written directly into console to guarantee client actions visibility (logging might not be configured in tests).
Example output:
[Client action]---------------------------------------------{
1 * Sending client request on thread main
1 > GET http://localhost:8080/sample/get
}----------------------------------------------------------
[Client action]---------------------------------------------{
1 * Client response received on thread main
1 < 200
1 < Content-Length: 13
1 < Content-Type: application/json
1 < Date: Mon, 27 Nov 2023 10:00:40 GMT
1 < Vary: Accept-Encoding
{"foo":"get"}
}----------------------------------------------------------
Console output might be disabled with a system proprty:
// shortcut sets DefaultTestClientFactory.USE_LOGGER property
DefaultTestClientFactory.disableConsoleLog()
With it, everything would be logged into ClientSupport
logger under INFO
(most likely, would be invisible in the most logger configurations, but could be enabled).
To reset property (and get logs back into console) use:
DefaultTestClientFactory.enableConsoleLog()
Note
Static methods added not directly into ClientSupport
because this is
the default client factory feature. You might use a completely different factory.
Custom client factory¶
JerseyClient
used in ClientSupport
could be customized now using TestClientFactory
implementation.
Simple factory example:
public class SimpleTestClientFactory implements TestClientFactory {
@Override
public JerseyClient create(final DropwizardTestSupport<?> support) {
return new JerseyClientBuilder()
.register(new JacksonFeature(support.getEnvironment().getObjectMapper()))
.property(ClientProperties.CONNECT_TIMEOUT, 1000)
.property(ClientProperties.READ_TIMEOUT, 5000)
.property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true)
.build();
}
}
Tip
See DefaultTestClientFactory
implementation (it's a bit more complex)
Custom factory could be specified directly in test annotation (junit 5, spock 2):
@TestDropwizardApp(value = MyApp.class, clientFactory = CustomTestClientFactory.class)
All other builders also support client factory as an optional parameter.
Improve generic testing¶
Generic run builder¶
Generic builder was added to simplify application testing without test framework. It supports almost everything as junit 5 extensions.
Example:
RunResult result = TestSupport.build(App.class)
.config("src/test/resources/path/to/test/config.yml")
.configOverrides("foo: 2", "bar: 12")
.hooks(new MyHook())
.runCore()
or with action:
Object serviceValue = TestSupport.build(App.class)
.config("src/test/resources/path/to/test/config.yml")
.configOverrides("foo: 2", "bar: 12")
.hooks(new MyHook())
.runWeb(injector -> {
return injector.getInstance(FooService.class).getSomething();
})
Builder provide simple listener support to simplify setup and cleanup logic (without test framework):
TestSupport.build(App.class)
.listen(new TestSupportBuilder.TestListener<>() {
public void setup(final DropwizardTestSupport<C> support) throws Exception {
// do before test
}
...
})
.runCore();
All listener methods are default so only required methods could be overridden.
TestSupport
run*
and "create" methods (coreApp
, webApp
) are powered now
with a new builder (and so many of them support string config overrides as parameter).
Run methods improvements¶
Extra run*
methods added for simple cases (the same could be achieved with a new builder).
Void methods now return RunResult
object containing DropwizardTestSupport
object and Injector
-
everything that could be required for assertions after application run:
RunResult<MyConfig> res = TestSupport.runCoreApp(MyApp.class);
Assertions.assertEquals(2, res.getConfiguration().foo);
Shortcut methods with config overrides:
RunResult<MyConfig> res = TestSupport.runCoreApp(MyApp.class,
"path/to/config.yml", // could be null
"foo: 2", "bar: 11");
Capture console output¶
There is now a utility to capture console output:
String out = TestSupport.captureOutput(() -> {
// run application inside
TestSupport.runWebApp(App.class, injector -> {
ClientSupport client = TestSupport.getContextClient();
// call application api endpoint
client.get("sample/get", null);
return null;
});
});
// uses assert4j, test that client was called (just an example)
Assertions.assertThat(out)
.contains("[Client action]---------------------------------------------{");
Returned output contains both System.out
and System.err
- same as it would be seen in console.
All output is also printed into console to simplify visual validation
Warning
Such tests could not be run in parallel (due to system io overrides)
Support commands testing¶
An easy way for testing commands was added: CommandTestSupport
object is
equivalent to DropwizardTestSupport
, but for running commands.
It uses dropwizard Cli
for arguments recognition and command selection.
The main difference with DropwizardTestSupport
is that command execution is
a short-lived process and all assertions are possible only after the execution.
That's why command runner would include in the result all possible dropwizard objects,
created during execution (because it would be impossible to reference them after execution).
New builder (very similar to application execution builder, described above) was added to simplify commands execution:
CommandResult result = TestSupport.buildCommandRunner(App.class)
.run("simple", "-u", "user")
Assertions.assertTrue(result.isSuccessful());
This runner could be used to run any command type (simple, configured, environment).
The type of command would define what objects would be present ofter the command execution
(for example, Injector
would be available only for EnvironmentCommand
).
Important
Such run never fails with an exception: any appeared exception would be stored inside the response:
Assertions.assertFalse(result.isSuccessful());
Assertions.assertEquals("Error message", result.getException().getMessage());
IO¶
Runner use System.in/err/out replacement. All output is intercepted and could be asserted:
Assertions.assertTrue(result.getOutput().contains("some text"))
result.getOutput()
contains both out
and err
streams together
(the same way as user would see it in console). Error output is also available
separately with result.getErrorOutput()
.
Note
All output is always printed to console, so you could always see it after test execution (without additional actions)
Commands requiring user input could also be tested (with mocked input):
CommandResult result = TestSupport.buildCommandRunner(App.class)
.consoleInputs("1", "two", "something else")
.run("quiz")
At least, the required number of answers must be provided (otherwise error would be thrown, indicating not enough inputs)
Warning
Due to IO overrides, command tests could not run in parallel.
For junit 5, such tests could be annotated with @Isolated
(to prevent execution in parallel with other tests)
Configuration¶
Configuration could be applied the same way as in run builder: direct configuration instance, file or (with) overrides:
// override only
TestSupport.buildCommandRunner(App.class)
.configOverride("foo: 12")
.run("cfg");
// file with overrides
TestSupport.buildCommandRunner(App.class)
.config("src/test/resources/path/to/config.yml")
.configOverride("foo: 12")
.run("cfg");
// direct config object
MyConfig config = new MyConfig();
TestSupport.buildCommandRunner(App.class)
.config(config)
.run("cfg");
Note
Config file should not be specified in command itself - builder would add it, if required.
But still, it would not be a mistake to use config file directly in command:
TestSupport.buildCommandRunner(App.class)
// note .config("...") was not used (otherwise two files would appear)!
.run("cfg", "path/to/config.yml");
Using builder for config file configuration assumed to be a preferred way.
Listener¶
There is a simple listener support (like in application run builder) for setup-cleanup actions:
TestSupport.buildCommandRunner(App.class)
.listen(new CommandRunBuilder.CommandListener<>() {
public void setup(String[] args) { ... }
public void cleanup(CommandResult<TestConfiguration> result) { ... }
})
.run("cmd")
Test application startup fail¶
Command runner could also be used for application startup fail tests:
CommandResult result = TestSupport.buildCommandRunner(App.class)
.run("server")
or with the shortcut:
CommandResult result = TestSupport.buildCommandRunner(App.class)
.runApp()
Note
In case of application successful start, special check would immediately stop it by throwing exception (resulting object would contain it), so such test would never freeze.
No additional mocks or extensions required because running like this would not cause
System.exist(1)
call, performed in Application
class (see Application.onFatalError
).