Spock 1 test support
NOTE: Module was extracted from guicey core. Package remains the same to simplify migration (only additional dependency would be required).
DEPRECATED because implementation relies on deprecated junit 4 rules. Consider migration to junit 5 (spock 2)
Maven:
<dependency>
<groupId>ru.vyarus.guicey</groupId>
<artifactId>guicey-test-spock</artifactId>
<version>{guicey.version}</version>
<scope>test</scope>
</dependency>
Gradle:
testImplementation 'ru.vyarus.guicey:guicey-test-spock:{guicey.version}'
Omit version if guicey BOM used.
OR you can use it with junit 5 vintage engine:
testImplementation 'ru.vyarus.guicey:guicey-test-spock:{guicey.version}'
testImplementation 'org.junit.jupiter:junit-jupiter-api'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter'
testRuntimeOnly 'org.junit.vintage:junit-vintage-engine'
This way you can write both spock (groovy) and junit 5 (java or groovy) tests.
Provided extensions:
@UseGuiceyApp
- for lightweight tests (without starting web part, only guice context)@UseDropwizardApp
- for complete integration tests
Both extensions allow using injections directly in specifications (like spock-guice).
@UseGuiceyHooks
extension could be used to apply configuration hook
common for all tests. But, it is deprecated in favor of native hooks support in main extensions.
@UseGuiceyApp
runs all guice logic without starting jetty (so resources, servlets and filters will not be available).
Managed
objects will still be handled correctly.
@UseGuiceyApp(MyApplication)
class AutoScanModeTest extends Specification {
@Inject MyService service
def "My service test" {
when: 'calling service'
def res = service.getSmth()
then: 'correct result returned'
res == 'hello'
}
Application started before all tests in annotated class and stopped after them.
@UseDropwizardApp
is useful for complete integration testing (when web part is required):
@UseDropwizardApp(MyApplication)
class WebModuleTest extends Specification {
@Inject MyService service
def "Check web bindings"() {
when: "calling filter"
def res = new URL("http://localhost:8080/dummyFilter").getText()
then: "filter active"
res == 'Sample filter and service called'
service.isCalled()
In order to start application on random port you can use configuration shortcut:
@UseDropwizardApp(value = MyApplication, randomPorts = true)
NOTE
Random ports will be applied even if configuration with exact ports provided:
@UseDropwizardApp(value = MyApplication,
config = 'path/to/my/config.yml',
randomPorts = true)
Also, random ports support both server types (default and simple)
Real ports could be resolved with ClientSupport object.
Normally, rest mapping configured with server.rootMapping=/something/*
configuration, but
if you don't use custom configuration class, but still want to re-map rest, shortcut could be used:
@UseDropwizardApp(value = MyApplication, restMapping="something")
In contrast to config declaration, attribute value may not start with '/' and end with '/*' - it would be appended automatically.
This option is only intended to simplify cases when custom configuration file is not yet used in tests (usually early PoC phase). It allows you to map servlet into application root in test (because rest is no more resides in root). When used with existing configuration file, this parameter will override file definition.
Any gucie bean may be injected directly into test field:
@Inject
SomeBean bean
This may be even bean not declared in guice modules (JIT injection will occur).
To better understand injection scopes look the following test:
@UseGuiceyApp(AutoScanApplication)
class InjectionTest extends Specification {
// instance remain the same between tests
@Shared @Inject TestBean sharedBean
// new instance injected on each test
@Inject TestBean bean
// the same context used for all tests (in class), so the same bean instance inserted before each test
@Inject TestSingletonBean singletonBean
def "Check injection types"() {
when: "changing state of injected beans"
sharedBean.value = 10
bean.value = 5
singletonBean.value = 15
then: "instances are different"
sharedBean.value == 10
bean.value == 5
singletonBean.value == 15
}
def "Check shared state"() {
expect: "shared bean instance is the same, whereas other one re-injected"
sharedBean.value == 10
bean.value == 0
singletonBean.value == 15 // the same instance was set before second test
}
// bean is in prototype scope
static class TestBean {
int value
}
@Singleton
static class TestSingletonBean {
int value
}
}
NOTE
Guice AOP will not work on test methods (because test instances not created by guice).
Both extensions prepare special jersey client instance which could be used for web calls. It is mostly useful for complete web tests to call rest services and servlets.
@InjectClient
ClientSupport client
It will also work in static fields or @Shared
fields.
Client object provides:
- Access to JerseyClient object (for raw calls)
- Shortcuts for querying main, admin or rest contexts (it will count the current configuration automatically)
- Shortcuts for base main, admin or rest contexts base urls (and application ports)
Example usages:
// 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()
TIP
All methods above accepts any number of strings which would be automatically combined into correct path:
client.targetRest("some", "other/", "/part")
would be correctly combined as "/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.
when: "calling rest service"
def res = client.targetRest("some").request().buildGet().invoke()
then: "response is correct"
res.status == 200
res.readEntity(String) == "response text"
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.basePathMain() // main context path (http://localhost:8080/)
client.basePathAdmin() // admin context path (http://localhost:8081/)
client.basePathRest() // rest context path (http://localhost:8080/)
Raw client usage:
// call completely external url
client.target("http://somedomain:8080/dummy/").request().buildGet().invoke()
WARNING
Client object could be injected with both dropwizard and guicey extensions, but in case of guicey extension,
only raw client could be used (because web part not started all other methods will throw NPE)
For both extensions you can configure application with external configuration file:
@UseGuiceyApp(value = MyApplication,
config = 'path/to/my/config.yml'
class ConfigOverrideTest extends Specification {
Or just declare required values:
@UseGuiceyApp(value = MyApplication,
configOverride = [
@ConfigOverride(key = "foo", value = "2"),
@ConfigOverride(key = "bar", value = "12")
])
class ConfigOverrideTest extends Specification {
Or use both at once (here overrides will override file values):
@UseGuiceyApp(value = MyApplication,
config = 'path/to/my/config.yml',
configOverride = [
@ConfigOverride(key = "foo", value = "2"),
@ConfigOverride(key = "bar", value = "12")
])
class ConfigOverrideTest extends Specification {
You can use hooks to customize application.
In both extensions annotation hooks could be declared with attribute:
@UseDropwizardApp(value = MyApplication, hooks = MyHook)
or
@UseGuiceyApp(value = MyApplication, hooks = MyHook)
Where MyHook is:
class MyHook implements GuiceyConfigurationHook {}
Alternatively, you can declare hook directly in test static field:
@EnableHook
static GuiceyConfigurationHook HOOK = { it.modules(new DebugModule()) }
Any number of fields could be declared. The same way hook could be declared in base test class:
class BaseTest extends Specification {
// hook in base class
@EnableHook
static GuiceyConfigurationHook BASE_HOOK = { it.modules(new DebugModule()) }
}
@UseGuiceyApp(value = App, hooks = SomeOtherHook)
class SomeTest extends BaseTest {
// Another hook
@EnableHook
static GuiceyConfigurationHook HOOK = { it.modules(new DebugModule2()) }
}
All 3 hooks will work.
WARNING
This extension is deprecated in favour of field hooks declarations.
@UseGuiceyHooks(MyBaseHook)
class BaseTest extends Specification {
}
@UseGuiceyApp(App)
class SomeTest extends BaseTest {}
NOTE
You can still use test specific hooks together with declared base hook
(to apply some more test-specific configuration).
WARNING
Only one @UseGuiceyHooks
declaration may be used in test hierarchy:
for example, you can't declare it in base class and then another one on extended class
- base for a group of tests. This is spock limitation (only one extension will actually work) but should not be an issue for most cases.
It is a common need to run multiple tests with the same test application configuration (same config overrides, same hooks etc.). Do not configure it in each test, instead move extension configuration into base test class:
@UsetGuiceyApp(...)
abstract class AbstractTest extends Specification {
// here might be helper methods
}
And now all test classes should simply extend it:
class Test1 extends AbstractTest {
@Inject
MyService service
def "Check something"() { ... }
}
StartupErrorRule
may be used to intercept dropwizard System.exit(1)
call.
But it will work different then for junit:
then
section is always called with exception (CheckExitCalled
).
Also, then
section may be used for assertion after exit calls and so there is
no need to add custom assertion callbacks (required by junit tests).
class ErrorTest extends Specification {
@Rule StartupErrorRule RULE = StartupErrorRule.create()
def "Check startup error"() {
when: "starting app with error"
new MyErrApp().main(['server'])
then: "startup failed"
thrown(RULE.indicatorExceptionType)
RULE.output.contains('stating application')
RULE.error.contains('some error occur')
class MyTest extends Specification {
@ClassRule @Shared
JunitRule sharedRule = new JunitRule()
@Rule
JunitRule2 rule = new JunitRule2()
def setupSpec() {
}
def setup() {
}
def "Test method body" () {
setup:
}
}
NOTE
Class rules are applied once per test class (same as setupSpec
).
Rules are applied per test method (same as setup
).
Setup order:
- Class rule
- Setup spec method
- Rule
- Setup method
- Test method's setup section
There is no special extensions for Spock 2, instead junit 5 integrations must be used with it, using special library
Current spock extensions are almost equivalent to junit5 extensions (in features and behaviour):
- Instead of
@UseGuiceyApp
use@TestGuiceyApp
- Instead of
@UseDropwizardApp
use@TestDropwizardApp
- Hooks can be specified with hooks declaration in extensions or as test fields
- Instead of
StartupErrorRule
use system-stubs - the successor of system rules
For ClientSupport
object, INSTEAD of
@Inject ClientSupport client
use parameter injection (possibly in fixture methods too):
def "Check something"(ClientSupport client) {}
Junit extension does not require an annotation for each override, so INSTEAD of:
@UseDropwizardApp(value = App, configOverride = [
@ConfigOverride(key = "server.rootPath", value = "/rest/*"),
@ConfigOverride(key = "server.applicationContextPath", value = "/prefix"),
@ConfigOverride(key = "server.adminContextPath", value = "/admin")
Use:
@TestDropwizardApp(value = App, restMapping = "/rest/*",
configOverride = [
"server.applicationContextPath: /prefix",
"server.adminContextPath: /admin"])
Note that server.rootPath
could be configured with restMapping
annotation property.
You may also use alternative declaration:
class MyTest extends Specification {
@RegisterExtension
static TestDropwizardAppExtension app = TestDropwizardAppExtension.forApp(App)
.config("src/test/resources/ru/vyarus/dropwizard/guice/config.yml")
.configOverrides("foo: 2", "bar: 12")
.randomPorts()
.hooks(Hook)
.hooks(builder -> builder.disableExtensions(DummyManaged))
.create()
}
This is an alternative to previous rules declaration in fields. It is useful when you need dynamic hook (as lambda) or configuration overrides require some other extensions.
Note that config override may be registered with Supplier
:
.configOverride("key", () -> { Somewhere.getValue()})
!!! warning
Don't use @Shared
fields instead of static - it wouldn't work!
Also non-static field declaration is not supported by junit extension.