General test tools¶
Test framework-agnostic tools. Useful when:
- There are no extensions for your test framework
- Assertions must be performed after test app shutdown (or before startup)
- Commands testing
Test utils:
TestSupport
- root utilities class, providing easy access to other helpersDropwizardTestSupport
- dropwizard native support for full integration testsGuiceyTestSupport
- guice context-only integration tests (without starting web part)CommandTestSupport
- general commands testsClientSupport
- web client helper (useful for calling application urls)
Important
TestSupport
assumed to be used as a universal shortcut: everything could be created/executed through it
so just type TestSupport.
and look available methods - no need to remember other classes.
Application run¶
Application could be started in 2 ways:
- Core - create only guice injector (without starting web services) - ideal for business logic testing (pretty fast)
- Web - full application start to test web endpoints (and complete flows)
The second case is handled by DropwizardTestSupport
and the first one by GuiceyTestSupport
object (which is an extension for DropwizardTestSupport
).
There is a generic builder to simplify work with these objects (provides all possible options for configuring these objects):
RunResult result = TestSupport.build(App.class)
.config("src/test/resources/path/to/test/config.yml")
.configOverrides("foo: 2", "bar: 12")
.hooks(new MyHook())
// run lightweight application (without web services)
.runCore()
Tip
RunResult
contains both DropwizardTestSupport
used for execution and Injector
instance.
In other words, everything required for performing assertions.
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())
// run full application
.runWeb(injector -> {
return injector.getInstance(FooService.class).getSomething();
})
Note
Builder methods are almost equal to junit 5 extension builder
Important
All run methods declared as throws Exception
. This was done to bypass original
exceptions instead of wrapping them inside runtime exceptions.
This should not be a problem: just add throws Exception
into test method signature
Configuration¶
Configuration could be applied in a different ways:
// with override values only
TestSupport.build(App.class)
.configOverride("foo: 12")
// file with overrides
TestSupport.build(App.class)
.config("src/test/resources/path/to/config.yml")
.configOverride("foo: 12")
// direct config object
MyConfig config = new MyConfig();
TestSupport.build(App.class)
.config(config)
Also, configuration source provider could be modified:
TestSupport.build(App.class)
.config("path/in/classpath/config.yml")
.configSourceProvider(new ResourceConfigurationSourceProvider())
There are also configuration shortcuts:
TestSupport.build(App.class)
.randomPorts()
.restMapping("api")
To randomize used application ports (overrides config file values) and apply a different rest mapping path.
When config overrides are used, they are always stored as system properties. This could be a problem for parallel tests execution. To overcome this, test-unique prefixes could be used:
TestSupport.build(App.class)
.configOverride("foo", "1")
.propertyPrefix("something")
Junit 5 extensions use test class (and sometimes method) name to generate unique prefixes.
Lifecycle listeners¶
Builder also support listeners registration in order to simulate setup/cleanup lifecycle methods, common for test frameworks:
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.
Warning
Builder could be used for support objects creation (buildCore()
, buildWeb()
) -
in this case listeners could not be used (only builder runner could properly process listeners).
Shortcuts¶
For simple cases, there are many builder shortcuts in TestSupport
class.
Support object construction:
DropTestSupport support = TestSupport.webApp(App.class,
"path/to/config.yml", "prop: 1", "prop2: 2");
GuiceyTestSupport support = TestSupport.coreApp(App.class,
"path/to/config.yml", "prop: 1", "prop2: 2");
Run:
RunResult result = TestSupport.runWebApp(App.class);
RunResult result = TestSupport.runWebApp(App.class,
"path/to/config.yml", "prop: 1", "prop2: 2");
Object value = TestSupport.runWebApp(App.class, injector - > {
return injector.getInstance(Service.class).getSmth();
});
Object value = TestSupport.runWebApp(App.class,
"path/to/config.yml", injector - > {
return injector.getInstance(Service.class).getSmth();
}, "prop: 1", "prop2: 2");
// ... and same methods for "coreApp"
All these methods are builder shortcuts (suitable for simple cases). I will use the builder call below, but it is equivalent to these shortcut methods (they just appeared before builder and were preserved for compatibility).
Tip
Context DropwizardTestSupport
and ClientSupport
objects could be statically referenced inside callback:
TestSupport.runWebApp(App.class, injector - > {
DropwizardTestSupport support = TestSupport.getContext();
ClientSupport client = TestSupport.getContextClient();
return null;
});
Asserting execution¶
To assert configuration or any guicey bean it would be enough to use run without callback:
RunResult<CfgType> result = TestSupport.build(App.class).runCore();
// direct configuratio instance
Assertions.assertEquals(12, result.getConfiguration().getProp1());
// any guice bean
Assertions.assertEquals(12, result.getBean(Configuration.class).getProp1());
Assertions.assertNotNull(result.getEnvironment());
Assertions.assertNotNull(result.getApplication());
Web-related assertions could be done inside callback:
SomeRsponseObject res = TestSupport.build(App.class).runWeb(injecor -> {
ClientSupport client = TestSupport.getContextClient()
return client.get("some", SomeRsponseObject.class);
});
Assertions.assertEquals("something", res.getField1())
Or multiple assertions could be done directly inside callback.
Raw test support objects¶
It may be required to use DropwizardTestSuppor
objects directly: for example, when before() and after()
calls must be performed in different methods (some test framework integration).
Objects could be created with builder:
GuiceyTestSupport core = TestSupport.build(App.class)
.buildCore();
DropwizardTestSupport web = TestSupport.build(App.class)
.buildWeb();
Note
GuiceyTestSupport extends DropwizardTestSupport
, so in both cases
DropwizardTestSupport
could be used as type.
There are also shortcut methods:
DropwizardTestSupport supportCore = TestSupport.coreApp(App.class,
"path/to/config.yml",
"prop: 1", "prop2: 2");
DropwizardTestSupport supportWeb = TestSupport.webApp(App.class,
"path/to/config.yml",
"prop: 1", "prop2: 2");
Support object usage:
support.before()
// test
support.after()
This is equivalent to:
TestSupport.run(support, injector -> {
// test
});
Other helper methods for support object (executed while the support object is active):
TestSupport.getInjector(support)
- obtain application injectorTestSupport.getBean(support, Key/Class)
- get guice beanTestSupport.injectBeans(support, target)
- inject annotated object fieldsTestSupport.webClient(support)
- constructClientSupport
object
Support object provides references for dropwizard objects:
support.getEnvironment();
support.getConfiguration();
support.getApplication();
Complete example using junit:
public class RawTest {
static DropwizardTestSupport support;
@Inject MyService service;
@BeforeAll
public static void setup() {
support = TestSupport.coreApp(App.class);
// support = TestSupport.webApp(App.class);
// start app
support.before();
}
@BeforeEach
public void before() {
// inject services in test
TestSupport.injectBeans(support, this);
}
@AfterAll
public static void cleanup() {
if (support != null) {
support.after();
}
}
@Test
public void test() {
Assertions.assertEquals("10", service.computeValue());
}
}
Note
support.before()
would automatically call after()
in case of startup error
Client¶
ClientSupport
is a JerseyClient
aware of dropwizard configuration, so you can easily call admin/main/rest urls.
Creation:
ClientSupport client = TestSupport.webClient(support);
where support is DropwizardTestSupport
or GuiceyTestSupport
(in later case it could be used only as generic client for calling external urls).
Example usage:
// GET {rest path}/some
client.targetRest("some").request().buildGet().invoke()
// GET {main context path}/servlet
client.targetMain("servlet").request().buildGet().invoke()
// GET {admin context path}/adminServlet
client.targetAdmin("adminServlet").request().buildGet().invoke()
// General external url call
client.target("https://google.com").request().buildGet().invoke()
Tip
All methods above accepts any number of strings which would be automatically combined into correct path:
client.targetRest("some", "other/", "/part")
As you can see, test code is abstracted from actual configuration: it may be default or simple server with any contexts mapping on any ports - target urls will always be correct.
Response res = client.targetRest("some").request().buildGet().invoke()
Assertions.assertEquals(200, res.getStatus())
Assertions.assertEquals("response text", res.readEntity(String))
Also, if you want to use other client, client object can simply provide required info:
client.getPort() // app port (8080)
client.getAdminPort() // app admin port (8081)
client.basePathRoot() // root server path (http://localhost:8080/)
client.basePathMain() // main context path (http://localhost:8080/)
client.basePathAdmin() // admin context path (http://localhost:8081/)
client.basePathRest() // rest context path (http://localhost:8080/)
Simple REST methods¶
The client also contains simplified GET/POST/PUT/DELETE methods for path, relative to server root (everything after port):
@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);
// post with result
Result res = client.post("rest/action", new PostObject(), Result.class);
}
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.
Tip
These methods could be used as examples for jersey client usage.
Customization¶
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.
Default client¶
JerseyClient
used inside ClientSupport
is created by DefaultTestClientFactory
.
Default implementation:
- Enables multipart feature if
dropwizard-forms
is in classpath (so the client could be used for sending multipart data). - Enables request and response logging to simplify writing (and debugging) tests.
By default, all request and response 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 (java.util) 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.
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)
Test commands¶
CommandTestSupport
object is a commands test utility equivalent to DropwizardTestSupport
.
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 (almost the same as application execution builder) 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 options are the same as in run builder. For example:
// 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
).