6.3.0 Release Notes¶
The release contains:
- Many improvements simplifying usage (main bundle, guicey bundles, shared state).
- New reports for measuring application startup performance.
- Private guice modules support
- Many test improvements (stubs, mocks, spies, lightweight REST testing etc.)
- Breaking shared state change (conceptual)
Un-deprecated HK2 support¶
All deprecation annotations removed for HK2 related features. Soft deprecation remain in javadoc.
It is possible to replace HK2 context with a child guice injector (conceptual prototype was ready long ago). But it requires A LOT of time to finish. Eventually, it would be finished, but, for now, other features are in priority.
Hard deprecation removed because there are no alternatives (and will not appear soon). Sorry for the inconvenience.
GuiceBundle builder¶
Configuration awareness¶
Before, GuiceyBundle
was required to perform any registrations, requiring configuration.
Now there is an alternative:
GuiceBundle.builder()
...
.whenConfigurationReady(env -> {
// env is GuiceyEnvironment, like in GuiceyBundle#run
AppConfig config = env.configuration();
env.modules(new GuiceModule(config));
});
Listener shortcuts¶
Listener shortcut methods added (same as in GuiceyEnvironment
):
GuiceBundle.builder()
...
// these listeners are shortcuts of guicey listeners (.listen())
// guice injector created (dropwizard run phase)
.onGuiceyStartup((config, env, injector) -> {
// just an example - there could be loginc, usually declared
// in application run method
env.jersey().register(injector.getInstance(SomeBean.class));
})
// executes after complete application startup (including guicey lightweight test)
.onApplicationStartup(injector -> ...)
//executes after application shutdown
.onApplicationShutdown(injector -> ...)
// these methods just for convenience:
// they actually use .onGuiceyStartup() to register provided listeners
.listenServer(server -> ...)
.listenJetty(new LifeCycle.Listener() {...})
.listenJersey(new ApplicationEventListener() {...})
Note
All the same listeners could be registered within .whenConfigurationReady(env -> env.listenServer(...)
,
but still they remain in the main bundle for more clarity (simpler to find, simpler to use in some cases)
Also, note that all these methods are available now for GuiceyConfigurationHook
.
For example, it could be used in tests:
@EnableHook
static GuiceyConfigurationHook hook = builder -> builder
.printStartupTime()
.onGuiceyStartup((config, env, injector) -> env.lifecycle().manage(...));
Diagnostic reports¶
Application startup time report¶
The new report intended to show the entire application startup time information to simplify searching for bottlenecks. It's hard to measure everything exactly from a bundle, but the report will try to show the time spent in each phase (init, run, web) and time of each registered dropwizard bundle.
GuiceBundle.builder()
.printStartupTime()
Sample output:
INFO [2025-03-27 09:12:27,435] ru.vyarus.dropwizard.guice.debug.StartupTimeDiagnostic: Application startup time:
JVM time before : 1055 ms
Application startup : 807 ms
Dropwizard initialization : 127 ms
GuiceBundle : 123 ms (finished since start at 127 ms)
Bundle builder time : 38 ms
Hooks processing : 3.23 ms
StartupDiagnosticTest$Test1$$Lambda/0x0000711de72a1d70: 2.37 ms
Classpath scan : 44 ms
Commands processing : 4.41 ms
DummyCommand : 0.42 ms
NonInjactableCommand : 3.16 ms
Bundles lookup : 1.15 ms
Guicey bundles init : 3.24 ms
WebInstallersBundle : 0.52 ms
CoreInstallersBundle : 1.83 ms
Installers time : 21 ms
Installers resolution : 15 ms
Scanned extensions recognition : 6.13 ms
Listeners time : 1.35 ms
ConfigurationHooksProcessedEvent : 0.23 ms
BeforeInitEvent : 0.59 ms
BundlesResolvedEvent : 0.009 ms
BundlesInitializedEvent : 0.43 ms
CommandsResolvedEvent : 0.006 ms
InstallersResolvedEvent : 0.01 ms
ClasspathExtensionsResolvedEvent : 0.009 ms
InitializedEvent : 0.007 ms
Dropwizard run : 679 ms
Configuration and Environment : 483 ms
GuiceBundle : 196 ms
Configuration analysis : 20 ms
...
Limitations
- Can't show init time of dropwizard bundles, registered before the guice bundle (obviously)
Applicaion#run
method time measured as part of "web" (the bundle can't see this point, but should not be a problem)
The report could be also enabled for compiled application: -Dguicey.hooks=startup-time
Guice provision time¶
The new report intended to show the time of guice beans provision (instance construction, including provider or provider method time). It shows all requested guice beans and the number of obtained instances (for prototype scopes).
GuiceBundle.builder()
.printGuiceProvisionTime()
All provisions are sorted by time:
INFO [2025-03-27 09:20:32,313] ru.vyarus.dropwizard.guice.debug.GuiceProvisionDiagnostic: Guice bindings provision time:
Overall 57 provisions took 1.40 ms
binding [@Singleton] ManagedFilterPipeline : 0.88 ms com.google.inject.servlet.InternalServletModule.configure(InternalServletModule.java:94)
binding [@Singleton] ManagedServletPipeline : 0.45 ms com.google.inject.servlet.InternalServletModule.configure(InternalServletModule.java:95)
providerinstance [@Singleton] @ScopingOnly GuiceFilter : 0.02 ms com.google.inject.servlet.InternalServletModule.provideScopingOnlyGuiceFilter(InternalServletModule.java:106)
JIT [@Prototype] JitService x10 : 0.02 ms (0.006 ms + 0.002 ms + 0.001 ms + 0.001 ms + 0.001 ms + ...)
binding [@Singleton] GuiceyConfigurationInfo : 0.01 ms ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule.configure(GuiceBootstrapModule.java:63)
binding [@Singleton] BackwardsCompatibleServletContextProvider : 0.007 ms com.google.inject.servlet.InternalServletModule.configure(InternalServletModule.java:99)
instance [@Singleton] Bootstrap : 0.004 ms ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule.bindEnvironment(GuiceBootstrapModule.java:71)
instance [@Singleton] @Config("server.gzip.minimumEntitySize") DataSize : 0.002 ms ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:129)
instance [@Singleton] Environment : 0.0009 ms ru.vyarus.dropwizard.guice.module.GuiceBootstrapModule.bindEnvironment(GuiceBootstrapModule.java:72)
instance [@Singleton] @Config AdminFactory : 0.0008 ms ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindUniqueSubConfigurations(ConfigBindingModule.java:117)
...
The report will also try to detect injection mistakes in case when JIT (just in time) binding is used when there are qualified declarations with the same type.
The most common mistake is configuration objects misuse: guicey binds unique configuration objects
with @Config
qualifier, but, if injection point declared without the qualifier,
guice will create a JIT binding (create new object instance) instead of injecting
declared instance. This might be hard to spot, especially when lombok is used (which may not
copy field annotation into constructor).
INFO [2025-03-27 09:21:33,438] ru.vyarus.dropwizard.guice.debug.GuiceProvisionDiagnostic: Guice bindings provision time:
Possible mistakes (unqualified JIT bindings):
@Inject Sub:
instance [@Singleton] @Config("val2") Sub : 0.0005 ms ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:129)
instance [@Singleton] @Marker Sub : 0.0007 ms ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindCustomQualifiers(ConfigBindingModule.java:87)
> JIT [@Prototype] Sub : 0.006 ms
@Inject Uniq:
instance [@Singleton] @Config Uniq : 0.0005 ms ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindUniqueSubConfigurations(ConfigBindingModule.java:117)
> JIT [@Prototype] Uniq : 0.004 ms
Overall 53 provisions took 1.45 ms
binding [@Singleton] ManagedFilterPipeline : 0.78 ms com.google.inject.servlet.InternalServletModule.configure(InternalServletModule.java:94)
binding [@Singleton] ManagedServletPipeline : 0.44 ms com.google.inject.servlet.InternalServletModule.configure(InternalServletModule.java:95)
In this example, the report detects incorrect injections:
@Inject
private Sub val;
@Inject
private Uniq uniq;
Detection will also work for generified bindings:
Possible mistakes (unqualified JIT bindings):
@Inject Service:
instance [@Singleton] Service<Integer> : 0.0006 ms ru.vyarus.dropwizard.guice.debug.provision.GenerifiedBindingsTest$App.lambda$configure$0(GenerifiedBindingsTest.java:46)
instance [@Singleton] Service<String> : 0.002 ms ru.vyarus.dropwizard.guice.debug.provision.GenerifiedBindingsTest$App.lambda$configure$0(GenerifiedBindingsTest.java:45)
> JIT [@Prototype] Service : 0.004 ms
The report could be also enabled for compiled application: -Dguicey.hooks=provision-time
The report shows only provisions performed on application startup, but it could be used in tests to detect provision problems at runtime:
@EnableHook
static GuiceProvisionTimeHook report = new GuiceProvisionTimeHook();
@Test
void testRuntimeReport() {
// clear startup data
report.clearData();
// do something that might cause additional provisions
injector.getInstance(Service.class);
injector.getInstance(Service.class);
// assert
Assertions.assertThat(report.getRecordedData().keys().size()).isEqualTo(2);
// or just print report (only for recorded provisions)
System.out.println(report.renderReport());
}
Shared state report¶
Guicey shared state is a bundle communication mechanism and safe "static" access for the important objects (quite rarely required). Before, it was not clear the real sequence of state population and access, and now there is a special report showing all state manipulations:
GuiceBundle.builder()
.printSharedStateUsage()
INFO [2025-03-27 09:49:35,219] ru.vyarus.dropwizard.guice.debug.SharedStateDiagnostic: Shared configuration state usage:
SET Options (ru.vyarus.dropwizard.guice.module.context.option) at r.v.d.g.m.context.(ConfigurationContext.java:167)
SET List (java.util) at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:60)
MISS at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:56)
GET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:57)
GET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:60)
GET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:61)
GET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:62)
GET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:73)
GET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:74)
GET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:82)
GET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:84)
SET Bootstrap (io.dropwizard.core.setup) at r.v.d.g.m.context.(ConfigurationContext.java:806)
SET Map (java.util) at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:97)
MISS at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:93)
GET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:94)
GET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:97)
GET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:98)
GET at r.v.d.g.d.SharedStateDiagnosticTest.(SharedStateDiagnosticTest.java:101)
...
Guice bindings report fixes¶
Guice bindings report (.printGuiceBindings()
or .printAllGuiceBindings()
) was fixed:
- Fixed scope accuracy for linked bindings
- Fixed bindings for private modules (missed exposed linked bindings)
GuiceyBundle¶
Checked initialization exceptions¶
Added throws Exception
in bundle init method:
public void initialize(final GuiceyBootstrap bootstrap) throws Exception {
Important
This is not a breaking change: all existing bundles will work (even compiled with
the previous guicey version). Existing runtime exception is re-thrown so even tests,
relying on the exception type or message would not break.
It's even possible to avoid throws Exception
in new bundles.
Initially, throws Exception
was not added to comply with dropwizard bundles (ConfiguredBundle
):
dropwizard assumes only runtime exceptions in initialization phase and any exception
in run phase.
But, it appears that often it is more useful to allow checked exceptions in init method to avoid clumsy exception handling (especially for quick prototyping) and so checked exceptions support was added. Runtime exceptions are rethrown as is.
Register extensions at run phase¶
Now extensions could be registered in run phase:
public class MyBundle implements GuiceyBundle {
@Override
public void run(final GuiceyEnvironment environment) throws Exception {
MyConfig config = environment.configuration();
if (config.isFeatureRequired()) {
environment.extensions(SomeExtension.class);
// OR
environment.extensionsOptions(SomeExtension.class);
}
}
}
Initially, it was assumed that all configuration should be done in the configuration phase. But there are cases when extension registration depends on configuration (or even configuration provides additional extensions).
Disabling already registered extensions also appears to not cover all cases.
Also, guicey already detects extensions from guice bindings in the run phase, so it was already registering extensions in the run phase.
Related internal changes (will not anything, just in case):
- Manually registered extensions now validated in run phase (and so
ManualExtensionsValidatedEvent
called at run phase) - As before, classpath scan is performed at configuration phase, but actual extensions registration appears at run phase (because manual extensions registered in priority)
Transitive guice bundles order¶
public class MyBundle implements GuiceyBundle {
@Override
public void initialize(final GuiceyBootstrap bootstrap) throws Exception {
// transitive bundle
boostrap.bundles(new MySubBundle());
}
}
Before, all transitive bundles were initialized after root bundles:
MySubBundle.initialize()
called after MyBundle.initialize()
(and all other root bundles).
At run phase bundles were executed in the same sequence: MyBundle.run()
then MySubBundle.run()
.
Usually, initialization order does not matter, but, in some cases, it is important to have the transitive bundle initialized immediately (for example, it could store some important value into shared state).
Also, both dropwizard bundles and guice modules immediately initialize transitives. To unify behavior, transitive guicey bundles now also initialize immediately.
So in example above, MySubBundle.initialize()
would be called just in time of the
bundle registration boostrap.bundles(new MySubBundle())
and, after registration
root bundle could rely on transitive bundle (initialized) state.
At run phase, the transitive bundle would also be called before the root:
MySubBundle.run()
then MyBundle.run()
.
Listeners shortcuts¶
Add listener registration shortcuts at bundle run:
@Override
public void run(final GuiceyEnvironment environment) throws Exception {
environment
// executes after application shutdown
.onApplicationShutdown(injector -> ...)
// jersey startup events and requests listener
.listenJersey(new ApplicationEventListener() {...})
}
Just a useful shortcuts.
Hook¶
Added throws Exception
for guicey hook:
public interface GuiceyConfigurationHook {
void configure(GuiceBundle.Builder builder) throws Exception;
}
Important
This is not a breaking change: all existing hooks will work (even compiled with
the previous guicey version). Existing runtime exception is re-thrown so even tests,
relying on the exception type or message would not break.
It's even possible to avoid throws Exception
in new hooks.
Avoiding checked exception is especially useful when writing test hooks.
Private guice modules support¶
Before guicey did not try to search extension inside private guice modules. But this is not quite correct because if extension is declared in private module and it would be also registered manually, then guicey would create another binding for it, which may cause conflicts.
Now guicey analyzes private modules too. For example:
public class PModule extends PrivateModule {
@Override
protected void configure() {
// ExtImpl is extension (recognition sign absent in interface)
bind(IExt.class).to(ExtImpl.class);
// extension exposed by interface
expose(IExt.class);
}
}
public interface IExt {... }
public class Ext implements IExt, Managed { ... }
Guicey would detect that ExtImpl
is an extension, and it is available (through exposed interface)
and so register it as an extension.
Important
Guicey rely on extension classes and so it would need direct extension access (to be able to call
Injector.getInstance(ExtImpl.class)
). By default, it is not possible (exposed only interface),
but guicey would change private module: it would add an additional expose
for ExtImpl
.
Also, as any guicey extension could be disabled, then .disable(ExtImpl.class)
would remove binding inside private module (works only for top-level private modules).
If you'll face any problems with private modules behavior, please report it.
Old behavior could be reverted with:
GuiceBundle.builder()
.option(GuiceyOptions.AnalyzePrivateGuiceModules, false)
Classpath scan¶
Extensions scan filters¶
By default, classpath scanner checks all available classes, and the only way to avoid extension
recognition is @InvisibleForScanner
annotation.
Now custom conditions could be specified:
GuiceBundle.builder()
.autoConfigFilter(ignoreAnnotated(Skip.class))
In this example, classes annotated with @Skip
would not be recognized.
Note
Assumed static import for ClassFilters
utility, containing the most common cases.
If required, raw predicate could be used:
.autoConfigFilter(cls -> !cls.isAnnotationPresent(Skip.class))
It is also possible now to implement spring-like approach when only annotated classes are recognized:
GuiceBundle.builder()
.autoConfigFilter(annotated(Component.class, Service.class))
Here only extensions annotated with @Component
or @Service
would be recognized.
Note
This filter affects only extension search: installers and commands search does not use filters (because it would be confusing and error-prone).
Tip
Multiple filters could be specified:
GuiceBundle.builder()
.autoConfigFilter(annotated(Component.class, Service.class))
.autoConfigFilter(ignoreAnnotated(Skip.class))
Auto config filter also affects extensions recognition from guice bindings and so could be used for ignoring extensions from bindings.
It is also possible now to exclude some sub-packages from classpath scan:
GuiceBundle.builder()
.enableAutoConfig("com.company.app")
.autoConfigFilter(ignorePackages("com.company.app.internal"))
Private extension classes¶
By default, guicey does not search extensions in protected and package-private classes:
public class Something {
static class Ext1 implements Managed {}
protected static class Ext2 implements Managed {}
}
Now searching such extensions could be enabled with:
GuiceBundle.builder()
.option(GuiceyOptions.ScanProtectedClasses, true)
Disable predicate¶
Disable predicate is useful for disabling a wide range of extensions:
GuiceBundle.builder()
.disable(Disables.inPackage("com.company.app.rest.stubs"))
But, before predicate was called too early: before actual installer is assigned to the extension item model and so it was impossible to disable extensions by installer.
Now extensions could be disabled by installer.
For convenience, new installer-related shortcuts added:
Disables.jerseyExtension()
Disables.webExtension()
(servlets, filters and jersey)Disables.installedBy(...)
Example usage:
@EnableHook
static GuiceyConfigurationHook hook = builder ->
builder.disable(installedBy(WebFilterInstaller.class));
Also, disable shortcuts for exact items type (Disables.module()
, Disabled.extension()
, etc.) now raise predicate
type to simplify chained usage:
builder.disable(module().and(ModuleItemInfo mod -> ! mod.isOverriding()));
Shared state¶
State key (BREAKING)¶
Before, it was recommended to use bundle class as a shared state key, but it appears to be very confusing.
Now shared state object class must be used as a key
This affects both state storing:
// before was: put(Bundle.class, new MyState())
state.put(MyState.class, new MyState());
And access:
// before was: MyState myState = state.get(Bundle.class);
MyState myState = state.get(MyState.class);
This way shared state usage becomes type-safe (impossible to use a wrong type for the stored object by mistake).
Options accessor¶
Shared state now holds a reference to guicey options values:
SharedConfigurationState.get(environment).get().getOptions()
Reactive access¶
Added reactive state access method (action called as soon as value would be set). This is mostly useful for the main guice bundle:
GuiceBundle.builder()
withSharedState(state ->
state.whenReady(MyState.class, mystate -> ...))
whenReady
action called either immediately, if value already present, or just after value
would be set. Listener calls could be seen on the new shared state report (same as never called listeners)
Test¶
Disable managed lifecycle¶
Added ability to disable managed objects lifecycle for lightweight guicey tests:
JUnit 5 extensions:
@TestGuiceyApp(.., managedLifecycle = false)
@RegisterExtension
static TestGuiceyAppExtension ext = TestGuiceyAppExtension.forApp(..)
...
.disableManagedLifecycle()
In this case, start/stop method would not be called for managed objects. This might be useful for disabling some heavy application initialization logic, defined in managed objects, but not required in tests.
Note
Application lifecycle will remain: events like onApplicationStartup
would still be
working (and all registered LifeCycle
objects would work). Only managed objects ignored.
Option is also available for core (non-junit) testing support:
new GuiceyTestSupport().disableManagedLifecycle()
TestSupport.build(App.class).runCoreWithoutManaged(..)
Manual configuration objects¶
It is now possible to manually construct configuration object instance in junit5 extension (for both lightweight and full app tests):
@RegisterExtension
static TestGuiceyAppExtension ext = TestGuiceyAppExtension.forApp(..)
.config(() -> new MyConfig())
...
Or in setup object:
@EnableSetup
static **TestEnvironmentSetup** setup = ext -> ext.config(() -> new MyConfig())
Important
Configuration overrides would not work with manually created configuration objects. Use configuration modifiers instead.
Config override for a single key¶
Added config override for a single key-value pair (for the setup object and extension builders):
@EnableSetup
static TestEnvironmentSetup setup = ext -> ext.configOverride("some.key", "12");
(before, only methods with supplier and multiple keys were available)
Configuration modifiers¶
Dropwizard configuration overrides mechanism is limited (for example, it would not work for a collection property).
Configuration modifier is an alternative mechanism when all changes are performed on configuration instance.
Modifier could be used as lambda:
@RegisterExtension
static TestGuiceyAppExtension ext = TestGuiceyAppExtension.forApp(..)
.configModifiers(config -> config.getSomething().setFoo(12))
...
Or in setup object:
@EnableSetup
static TestEnvironmentSetup setup = ext ->
ext.configModifiers(config -> config.getSomething().setFoo(12))
Modifier could be declared in class:
public class MyModifier implements ConfigModifier<MyConfig> {
@Override
public void modify(MyConfig config) throws Exception {
config.getSomething().setFoo(12);
}
}
@TestGuiceyApp(.., configModifiers = MyModifier.class)
Tip
Modifier could be used with both manual configuration or usual (yaml) configuration. Configuration modifiers also could be used together with configuration overrides.
Limitation
Configuration modifiers are called after dropwizard logging configuration, so logging is the only thing that can't be configured (use configuration overrides for logging)
Configuration overrides are also available in core test extensions:
- Commands runner:
TestSupport.buildCommandRunner(..).configModifiers(...)
- Raw test support builder:
TestSupport.build(..).configModifiers(...)
GuiceyTestSupport.configModifiers(..)
- For
DropwizardTestSupport
custom command must be used to support modifiers:ConfigOverrideUtils.buildCommandFactory
Custom configuration block¶
To simplify field-based declarations, custom (free) block added (.with()
):
@RegisterExtension
static TestGuiceyAppExtension ext = TestGuiceyAppExtension.forApp(..)
...
.with(builder -> {
if (...) {
builder.configOverrides("foo.bar", 12);
}
})
And the same for setup objects:
@EnableSetup
static TestEnvironmentSetup setup = ext ->
...
.with(builder -> {
...
})
Debug option evolution¶
Existing junit extensions debug mechanism was evolved:
@TestGuiceyApp(.., debug = true)
Extensions time¶
To simplify slow tests (slowness) investigations, guicey now measures and prints extensions time.
For example, test with application started in beforeAll, with two test methods (same app for both tests):
@TestGuiceyApp(value = App.class, debug = true)
public class PerformanceLogTest {
@Test
public void test1() { ... }
@Test
public void test2() { ... }
}
\\\------------------------------------------------------------/ test instance = 1595d2b2 /
Guicey time after [Before each] of PerformanceLogTest#test1(): 1204 ms
[Before all] : 1204 ms
Guicey fields search : 2.03 ms
Guicey hooks registration : 0.02 ms
Guicey setup objects execution : 1.92 ms
DropwizardTestSupport creation : 1.47 ms
Application start : 1172 ms
[Before each] : 0.46 ms
Guice fields injection : 0.19 ms
\\\------------------------------------------------------------/ test instance = 45554613 /
Guicey time after [Before each] of PerformanceLogTest#test2(): 1205 ms ( + 0.33 ms)
[Before each] : 0.69 ms ( + 0.23 ms)
Guice fields injection : 0.36 ms ( + 0.17 ms)
[After each] : 0.10 ms
\\\---------------------------------------------------------------------------------------------
Guicey time after [After all] of PerformanceLogTest: 1207 ms ( + 2.15 ms)
[After each] : 0.11 ms ( + 0.01 ms)
[After all] : 2.14 ms
Application stop : 1.72 ms
There are three reports:
- Before first test method (see guicey extension startup time)
- Before the second test method (see guicey time for the second method only)
- After all (cleanup time)
Only the first report shows all recorded times, next reports only mention time increase.
For example, the second report mentions only Guice fields injection : 0.36 ms ( + 0.17 ms)
Meaning guicey perform fields injection just before the second test, spent 0.17 ms on it
(overall injection time for two injections is 0.36 ms)
Updated declarations report¶
All declared setup objects and hooks now showed with a (declaration) source reference (where possible). Simplified report for lambdas (was not very readable before).
public static class Test2 extends Base {
@RegisterExtension
static TestGuiceyAppExtension app = TestGuiceyAppExtension.forApp(App.class)
.setup(Ext1.class, Ext2.class)
.setup(it -> null, new Ext3())
.debug()
.create();
@EnableSetup
static TestEnvironmentSetup ext1 = it -> null;
@EnableSetup
static TestEnvironmentSetup ext2 = it -> null;
Guicey test extensions (Test2.):
Setup objects =
Ext1 @RegisterExtension.setup(class) at r.v.d.g.t.j.d.SetupObjectsLogTest.(SetupObjectsLogTest.java:102)
Ext2 @RegisterExtension.setup(class) at r.v.d.g.t.j.d.SetupObjectsLogTest.(SetupObjectsLogTest.java:102)
<lambda> @RegisterExtension.setup(obj) at r.v.d.g.t.j.d.SetupObjectsLogTest.(SetupObjectsLogTest.java:103)
Ext3 @RegisterExtension.setup(obj) at r.v.d.g.t.j.d.SetupObjectsLogTest.(SetupObjectsLogTest.java:103)
<lambda> @EnableSetup Base#base1 at r.v.d.g.t.j.d.SetupObjectsLogTest$Base#base1
<lambda> @EnableSetup Base#base2 at r.v.d.g.t.j.d.SetupObjectsLogTest$Base#base2
<lambda> @EnableSetup Test2#ext1 at r.v.d.g.t.j.d.SetupObjectsLogTest$Test2#ext1
<lambda> @EnableSetup Test2#ext2 at r.v.d.g.t.j.d.SetupObjectsLogTest$Test2#ext2
Inject test fields once¶
By default, guicey would inject test field values before every test method, even if the same
test instance used (TestInstance.Lifecycle.PER_CLASS
). This should not be a problem
in the majority of cases because guice injection takes very little time.
Also, it is important for prototype beans, which will be refreshed for each test.
Now it is possible to inject fields just once:
@TestGuiceyApp(value = App.class, injectOnce = true)
// by default new test instance used for each method, so injectOnce option would be useless
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class PerClassInjectOnceGuiceyTest {
@Inject
Bean bean;
@Test
public test1() {..}
@Test
public test2() {..}
}
In this case, the same test instance used for both methods (Lifecycle.PER_CLASS
)
and Bean bean
field would be injected just once (injectOnce = true
)
Tip
To check the actual fields injection time enable debug (debug = true
) and
it will print injection time before each test method:
[Before each] : 2.05 ms
Guice fields injection : 1.58 ms
Setup objects evolution¶
Added throws Exception
in setup object method:
public class GuiceyEnvironmentSetup {
public Object setup(TestExtension extension) throws Exception { }
}
Important
This is not a breaking change: all existing setup objects will work (even compiled with
the previous guicey version). Existing runtime exception is re-thrown so even tests,
relying on the exception type or message would not break.
It's even possible to avoid throws Exception
in new objects.
With all extensions below, setup objects could completely replace some native junit extensions: meaning it would be simpler now to implement just a setup object instead of separate junit extension implementation.
Junit context¶
Junit context is now directly accessible in builder:
public class MyExt implements GuiceyEnvironmentSetup {
@Override
public Object setup(TestExtension extension) throws Exception {
ExtensionContext context = extension.getJunitContext();
}
}
Also, ExtensionContext
could be injected as test method parameter:
@BeforeEach
public void setUp(ExtensionContext context) {
...
}
Lifecycle listeners¶
Added lifecycle listener interface, representing junit phases (before/after) and tested application (started/stopped):
public interface TestExecutionListener {
default void starting(final EventContext context) throws Exception {}
default void started(final EventContext context) throws Exception {}
default void beforeAll(final EventContext context) throws Exception {}
default void beforeEach(final EventContext context) throws Exception {}
default void afterEach(final EventContext context) throws Exception {}
default void afterAll(final EventContext context) throws Exception {}
default void stopping(final EventContext context) throws Exception {}
default void stopped(final EventContext context) throws Exception {}
}
EventContext
provides access for guice injector, DropwizardTestSupport object and junit 5 context.
Raw listener is useful for large setup objects:
public class MySetup implements TestEnvironmentSetup, TestExecutionListener {
@Override
public Object setup(TestExtension extension) throws Exception {
extension.listen(this);
}
@Override
public void started(final EventContext context) throws Exception {
// something
}
}
For field declarations (lambda-based), special individual shortcuts are available:
@EnableSetup
static TestEnvironmentSetup setup = ext -> ext
.onApplicationStarting(event -> ...)
.onApplicationStart(event -> ...)
.onBeforeAll(event -> ...)
.onBeforeEach(event -> ...)
.onAfterEach(event -> ...)
.onAfterAll(event -> ...)
.onApplicationStopping(event -> ...)
.onApplicationStop(event -> ...)
Extension debug¶
For simplicity, setup object could re-use guicey extension debug trigger to print additional data:
@TestGuiceyApp(value = App.class, stup = MySetup.class, debug = true)
public class MySetup implements TestEnvironmentSetup, TestExecutionListener {
@Override
public Object setup(TestExtension extension) throws Exception {
extension.listen(this);
if (extension.isDebug()) {
System.out.println("Debug info: ...");
}
}
@Override
public void started(final EventContext context) throws Exception {
if (context.isDebug()) {
System.out.println("Debug info: ...");
}
}
Annotated fields search API¶
Added .findAnnotatedFields()
method to search annotated test fields.
public class Test {
@MyAnn
Base field;
}
public class CustomFieldsSupport implements TestEnvironmentSetup {
@Override
public Object setup(TestExtension extension) throws Exception {
List<AnnotatedField<MyAnn, Base>> fields = extension
.findAnnotatedFields(MyAnn.class, Base.class);
}
Out of the box, API provides many checks, like required base class (it could be Object to avoid check): if annotated field type is different - error would be thrown.
Returned object is also abstraction: AnnotatedField
- it simplifies working with filed value,
plus contains additional checks.
Note
This api is not generalized (target usage with junit only) because it uses junit annotations search utility internally.
Added AnnotatedTestFieldSetup
base class which implements base fields workflow
(including proper nested tests support).
All new field extensions (see below) are using this base class. Using them as implementation examples, it should be pretty simple to add other field-based test extensions.
Auto lookup¶
Custom TestEnvironmentSetup
objects could be loaded automatically now
with service loader. New default extensions already use service loader.
To enable automatic loading of custom extension add:
META-INF/services/ru.vyarus.dropwizard.guice.test.jupiter.env.TestEnvironmentSetup
And put there required setup object classes (one per line), like this:
ru.vyarus.dropwizard.guice.test.jupiter.ext.log.RecordedLogsSupport
ru.vyarus.dropwizard.guice.test.jupiter.ext.rest.RestStubSupport
ru.vyarus.dropwizard.guice.test.jupiter.ext.stub.StubsSupport
ru.vyarus.dropwizard.guice.test.jupiter.ext.mock.MocksSupport
Now, when setup objects have more abilities, more custom test extensions could be implemented (see new filed-based extensions below). Automatic installation for such 3rd party extensions (using service loader) should simplify overall usage.
Note
Service loading for extensions could be disabled (together with new default extensions):
@TestGuiceyApp(.., useDefaultExtensions = false)
New test extensions¶
All new extensions are enabled by default: if any of them will cause any problems, please report. New extensions could be disabled with (this will also switch off an extension lookup mechanism)
@TestGuiceyApp(.., useDefaultExtensions = false)
After disabling, some extensions could be registered manually (they are all just setup objects).
Tip
All recognized extension fields would be printed to console when guicey extension debug
is enabled (debug = true
).
New extensions:
- Stubs (with deep injection)
- Mockito mocks (with deep injection)
- Mockito spies
- Guice bean calls tracker (method arguments and return value captor + time measure)
- Logs test support
- Lightweight REST tests support
Note
The core logic for all extensions is implemented as guicey hook. These hooks are generic and could be used without junit (see general docs)
Stubs¶
Stub is a (manual) replacement for the real bean.
For example, suppose application has a BillingService
, but for tests you
want to replace it with a simple (no-external-communication) implementation:
StubBillingService extends BillingService
.
Stubs declared in test class with a new @StubBean
annotation:
@TestGuiceyApp
public class Test {
@StubBean(BillingService.class)
StubBillingService stub;
}
In this example, stub is instantiated with guice (but you could do it manually) and
override BillingService
injection in guice context (using guice modules override).
That means that stub would not only be available in the annotated field, but all guice
beans, requiring BillingService
would actually receive stub instance instead of it.
If you try to declare service injection in test:
@Inject
BillingService billingService;
You'll see that it's the same instance as a stub.
Note
As a stub object injected into guice context, which by default used "per class",
the same stub instance would be used for all methods in test (of course, if guice extension
not declared "per method"). A stub object could implement special interface StubLifecycle
(with before/after methods) which would be called before and after each test method
(could be used to reset state).
Mocks¶
Requires mockito dependency (version not required if dropwizard (or guicey) BOM used):
testImplementation 'org.mockito:mockito-core'
Essentially, mocks are automatic stubs with the ability to dynamically declare method behavior.
Mocks declared with a @MockBean
annotation:
@TestGuiceyApp(...)
public class Test {
@MockBean
MyService mock;
@BeforeEach
void setUp() {
when(mock.foo()).thenReturn("something");
}
}
Mock instance created automatically (Mockito.mock(MyService.class)
) and override
MyService
declaration (using guice modules override).
Important
The main difference with mockito junit extension is that mock instance would be injected in all places using mocked service (replace real service in the entire application)
Mock behavior could be declared in test setup method or just before usage in test method.
If you try to declare service injection in test:
@Inject
MyService service;
You'll see that it's the same instance as a mock.
Note
As a mock object injected into guice context, which by default used "per class",
the same stub instance would be used for all methods in test (of course, if guice extension
not declared "per method"). A mock would be reset (Mockito.reset(mock)
) automatically
after each test method.
Mockito provides a mock usage report (Mockito.mockingDetails(value).printInvocations()
),
which could be enabled with @MockBean(printSummary = true)
(report shown after each test method):
\\\------------------------------------------------------------/ test instance = 6d420cdd /
@MockBean stats on [After each] for MockSummaryTest$Test1#test():
[Mockito] Interactions of: Mock for Service, hashCode: 1340267778
1. service.foo(1);
-> at ru.vyarus.dropwizard.guice.test.jupiter.setup.mock.MockSummaryTest$Test1.test(MockSummaryTest.java:55)
- stubbed -> at ru.vyarus.dropwizard.guice.test.jupiter.setup.mock.MockSummaryTest$Test1.setUp(MockSummaryTest.java:50)
Spies¶
Requires mockito dependency (version not required if dropwizard (or guicey) BOM used):
testImplementation 'org.mockito:mockito-core'
Spy is a partial mock: real service instance is proxied. Spies could be used to track instance calls or to modify some method behavior.
Spies declared with a @SpyBean
annotation:
@TestGuiceyApp(...)
public class Test {
@SpyBean
MyService spy;
// just for example (not required)
@Inject
MyService service;
// optional
@BeforeEach
void setUp() {
// important declaration reversed (otherwise real method is called during declaration)!
doReturn(12).when(spy).foo();
}
@Test
public void test() {
// calling methods on injected service to show that it is also a spy
// (normally spies would be called indirectly (by other beans))
Assertions.assertEquals(12, service.foo());
Assertions.assertEquals(1, service.bar(1));
// foo() (mocked method) called once
Mockito.verify(spy, Mockito.times(1)).foo();
// bar(1) called once
Mockito.verify(spy, Mockito.times(1)).bar(1);
}
}
Spy instance created automatically (Mockito.spy(service)
) and override
MyService
declaration (using guice AOP).
In the example above, method foo()
is mocked and method bar(..)
is not (original service method called).
Limitation
Spy objects would work only for beans, created by guice. Spy creation requires original service instance, and so guice AOP used to intercept call the original bean, create spy dynamically, and redirect calls to the spy object.
Note
As a spy object tied with guice context, which by default used "per class",
the same spy instance would be used for all methods in test (of course, if guice extension
not declared "per method"). A spy would be reset (Mockito.reset(mock)
) automatically
after each test method.
Same as for mocks, a usage report could be printed after each test @SpyBean(printSummary = true)
\\\------------------------------------------------------------/ test instance = 285bf5ac /
@SpyBean stats on [After each] for SpySummaryTest$Test1#test():
[Mockito] Interactions of: ru.vyarus.dropwizard.guice.test.jupiter.setup.spy.SpySummaryTest$Service$$EnhancerByGuice$$60e90c@40fe8fd5
1. spySummaryTest$Service$$EnhancerByGuice$$60e90c.foo(
1
);
-> at ru.vyarus.dropwizard.guice.test.jupiter.setup.spy.SpySummaryTest$Test1.test(SpySummaryTest.java:50)
Tip
Spies (in some cases) are not very easy to use for tracking method arguments and return values.
@TrackBean
extension could be more useful for actual values tracking
(could be used together with spies or mocks).
Example of how to intercept method call result:
public static class ResultCaptor<T> implements Answer {
private T result = null;
public T getResult() {
return result;
}
@Override
public T answer(InvocationOnMock invocationOnMock) throws Throwable {
result = (T) invocationOnMock.callRealMethod();
return result;
}
}
ResultCaptor<String> resultCaptor = new ResultCaptor<>();
Mockito.doAnswer(resultCaptor).when(spy).foo();
Assertions.assertThat(resultCaptor.getResult()).isEqualTo("something");
Trackers¶
Tracker extension was added to simplify checking guice beans method arguments and result value, because mockito spies are not very handy (see example above).
Tracker is declared with @TrackBean
, but field type must be Tracker<Service>
with target
service, declared as generic:
public class Test {
@TrackBean
Tracker<MyService> tracker;
@Inject
MyService service;
@Test
public void test() {
// call
service.foo("something");
MethodTrack track = serviceTracker.getLastTrack();
Assertions.assertTrue(track.toString().contains("foo(\"something\")"));
Assertions.assertEquals("something", track.getArguments()[0]);
Assertions.assertNull(track.getResult());
}
}
Limitation
Trackers implemented with guice AOP and so would work only for beans, created by guice.
As method arguments (and return value) could contain mutable objects (which would lost
the original state after several method calls), the tracker keeps both
raw object version and string version: getRawResult()
and getResult()
,
getRawArguments()
and getArguments()
.
Additionally, there are quoted versions: getQuatedResult()
and getQuatedArguments()
.
These methods are the same as string methods, but all strings are in quotes to clearly see
string bounds (quoted versions useful for console printing)
When too many method calls are tracked, keeping raw objects might lead to a waste of memeory,
in this case only string values could be preserved: @TrackBean(keepRawObjects = false)
Note
By default, tracker data would reset after each test method.
Use @TrackBean(autoRest = false)
to keep all data.
Tracked data could be reset manually at any time with: tracker.clear()
Searching¶
All recorded method calls could be obtained with List<MethodTrack> tracks = tracker.getTracks()
.
If there was just one call: MethodTrack track = tracker.getLastTrack()
In the case of many recorded executions, search could be used:
// search by method (any argument value)
tracks = tracker.findTracks(mock -> when(
mock.foo(Mockito.anyInt()))
);
// search methods with exact argument
tracks = tracker.findTracks(mock -> when(
mock.foo(Mockito.intThat(argument -> argument == 1)))
);
This method uses Mockito stubbing abilities for search criteria declaration: easy to use and type-safe search.
Performance metrics¶
There is a trace mode printing all method calls into console: @TrackBean(trace = true)
:
\\\---[Tracker<Service>] 0.11 ms <@71370fec> .foo(1) = "1 call"
Shows: instance hash, execution time, arguments and return value.
By default, tracker would only log executions for slow methods (more than 5 seconds):
WARN [2025-04-01 09:22:28,965] ru.vyarus.dropwizard.guice.test.jupiter.ext.track.Tracker:
\\\---[Tracker<Service>] 2.07 ms <@53aa2fc9> .foo() = "foo"
Note that warning is printed with a logger, and not as direct console output like trace.
Slow methods configuration: @TrackBean(slowMethods = 5, slowMethodsUnit = ChronoUnit.SECONDS)
(0
value to disable warnings).
When guice extension debug is enabled (@TestGuiceyApp(..., debug = true)
), a performance report
for all registered tracker objects would be printed:
\\\------------------------------------------------------------/ test instance = 2bbb44da /
Trackers stats (sorted by median) for TrackerSimpleTest#testTracker():
[service] [method] [calls] [fails] [min] [max] [median] [75%] [95%]
Service foo(int) 3 0 0.011 ms 0.161 ms 0.151 ms 0.161 ms 0.161 ms
Service bar(int) 1 0 0.066 ms 0.066 ms 0.066 ms 0.066 ms 0.066 ms
The report use dropwizard metrics to count percentiles. This is required to collect more informative data when trying to use trackers for performance testing - due to jvm warm-up first executions would be much slower, but percentiles should show more correct values (closer to hot execution).
Tip
For each tracker, an individual report could be activated with: @TrackBean(printSummary = true)
This report does not depend on the extension debug flag.
The summary report also shows the number of service instances involved in stats (in the example trace was enabled for clarity):
\\\---[Tracker<Service>] 0.28 ms <@6707a4bf> .foo(1) = "foo1"
\\\---[Tracker<Service>] 0.007 ms <@79d3473e> .foo(2) = "foo2"
\\\------------------------------------------------------------/ test instance = 51f18e31 /
Tracker<Service> stats (sorted by median) for ReportForMultipleInstancesTest$Test1#testTracker():
[service] [method] [calls] [fails] [min] [max] [median] [75%] [95%]
Service foo(int) 2 (2) 0 0.007 ms 0.281 ms 0.281 ms 0.281 ms 0.281 ms
Note different instances in trace (<@6707a4bf>
, <@79d3473e>
) and instances count in calls column: 2 (2)
Method stats (summary of all collected executions for method) could also be used for assertions:
TrackerStats stats = tracker.getStats();
Assertions.assertEquals(1, stats.getMethods().size());
MethodSummary summary = stats.getMethods().get(0);
Assertions.assertEquals("foo", summary.getMethod().getName());
Assertions.assertEquals(Service.class, summary.getService());
Assertions.assertEquals(1, summary.getTracks());
Assertions.assertEquals(0, summary.getErrors());
Assertions.assertEquals(1, summary.getMetrics().getValues().length);
Assertions.assertTrue(summary.getMetrics().getMin() < 1000);
REST stubs¶
Guicey provides lightweight REST testing (the 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).
Lightweight REST could be declared with @StubRest
annotation under @TestGuiceyApp
extension:
@TestGuiceyApp(...)
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:
@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
:
@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(...)
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, @StubRest
would use grizzly, if it's available on classpath or in-memory.
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 @StubRest
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.
Testing logs¶
Important
Logs testing works only with logback!
@RecordLogs
extension could record log messages for one or multiple classes:
@TestGucieyApp(...)
public class Test {
@RecordLogs(value = Service.class, level = Level.DEBUG)
RecordedLogs logs;
@Inject
Service service;
@Test
public void test() {
// here some actions with service, involving logging
service.doSomething();
Assertions.assertEquals(2, logs.count());
Assertions.assertTrue(logs.has(Level.DEBUG));
Assertions.assertEquals(Arrays.asList("message 1", "message 2"),
logs.messages());
}
}
Logs could be collected for any custom logger name or entire package:
@RecordLogs(loggers = "ru.vyarus.dropwizard.guice.test", level = Level.TRACE)
RecordedLogs logs;
Important
To collect logs, logger level must be set to the required level and so these logs would also appear in console output. This allows using log recorder as a simple way to enable required logs for tests.
Dropwizard resets logging two times during startup, but the extension also configures required loggers multiple times. In most cases, all required logged messages should be intercepted.
To avoid tons of selection methods with different parameters, all selection methods return sub-selector object for further selections. For example, to select messages by level and logger:
logs.logger(SomeClass.class).level(Level.DEBUG).messages().
Also, original event objects are available: .events()
.
Internal¶
BeforeInit event¶
Added BeforeInitEvent
- ("meta" event) the first point where Bootstrap
reference is available
(GuiceBundle
initialization started), but guicey actually did not start any actions, except
hooks processing.
Example usage:
GuiceBundle.builder()
.listen(new GuiceyLifecycleAdapter(){
@Override
protected void beforeInit(final BeforeInitEvent event) { ... }
})
Event used by the new startup time report (to modify Bootstrap
object for execution
time tracking).
Web installers marker¶
Added a new marker interface WebInstaller
. It must be applied for all
web (jetty) and jersey related installers.
Marker used to mark extensions as web extensions and be able to disable them all at once (for example):
@EnableHook
static GuiceyConfigurationHook hook = builder ->
builder.disable(webExtension());
This is used in the new rest stubs to disable web extensions (not started with the jersey test support to avoid confusing console output).
Migration guide¶
- If guicey shared state was used, then you'll have to update places accessing stored objects: before it was recommended to use bundle name as a key, now stored object class must be used (initial approach was not very easy to use (even confusing) now access is type-safe)
- Guicey now analyzes bindings exposed from private guice modules to detect extensions.
Also, disabling extension, declared in private module would lead to private binding remove.
If any problems appear, old behavior could be restored with:.option(GuiceyOptions.AnalyzePrivateGuiceModules, false)
- If junit 5 used for tests, pay attention to new features:
- Configuration modifiers (could be more useful than the configuration overrides mechanism)
GuiceyEnvironmentSetup
objects now have direct access to junit context and test lifecycle events and so could completely replace some junit extensions (where guicey integration is required)- New field-based extensions:
*
@StubBean
- replacing guice beans with custom stubs (without additional modules declaration) *@MockBean
- replacing guice beans with Mockito mocks *@SpyBean
- replacing guice beans with Mockito spies *@TrackBean
- simpler alternative to Mockito spies to track guice beans methods calls *@StubRest
- lightweight rest services testing (without starting web) *@RecordLogs
- test logs