Junit 5 (jupiter) extensions support for Spock Framework 2: allows using junit 5 extension in spock (like it was with junit 4 rules in spock 1).
Features:
- Supports almost all junit extension types. Problem may appear only with extensions requiring
TestInstanceFactory
which is impossible to support because spock manage test instance itself.- Warning message would indicate not supported extension types (if usage detected)
- Supports all the same registration types:
- @ExtendWith on class, method, field or param
- Custom annotations on class, method, field or param
- @RegisterExtension on static and usual fields (direct registration)
- Supports parameters injection in test and fixture methods (but not in constructor)
- Support junit
ExecutionConditions
(for example, junit@Disabled
would work) - Implicit activation: implemented as global spock extension
- API for spock extensions to use junit value storage (to access values stored by junit extensions or for direct usage because spock does not provide such feature)
Extensions behaviour is the same as in jupiter engine (the same code used where possible). Behavior in both engines validated with tests.
Originally developed for dropwizard-guicey to avoid maintaining special spock extensions.
Spock 1 provides spock-junit4 module to support junit 4 rules. At that time spock extensions model was "light years" ahead of junit. But junit 5 extensions are nearly equal in power to spock extensions (both has pros and cons). It is a big loss for spock to ignore junit5 extensions: junit extensions are easier to write, but spock is still much better for writing tests.
Module named spock-junit5
by analogy with legacy spock-junit4
module.
There should be no official spock-junit5
module so name would stay unique
(there are discussions about official spock-jupiter
module with value storage implementation).
More details about motivation and realization in the blog post.
Maven:
<dependency>
<groupId>ru.vyarus</groupId>
<artifactId>spock-junit5</artifactId>
<version>1.2.0</version>
</dependency>
Gradle:
implementation 'ru.vyarus:spock-junit5:1.2.0'
Compiled for java 8 (compatible up to java 17), junit 5.9.1
The only transitive library dependency is junit-jupiter-api: first of all, to bring in required junit annotations and to prevent usage with lower junit versions
Spock | Junit | Version |
---|---|---|
2.2 | 5.9.1 | 1.2.0 |
2.1 | 5.8.2 | 1.0.1 |
2.0 | 5.7.2 | 1.0.1 |
You can use snapshot versions through JitPack:
- Go to JitPack project page
- Select
Commits
section and clickGet it
on commit you want to use (top one - the most recent) - Follow displayed instruction: add repository and change dependency (NOTE: due to JitPack convention artifact group will be different)
Junit 5 extensions could be applied as usual:
@ExtendWith([Ext0, Ext1])
class Test extends Specification {
@ExtendWith(Ext2)
static Integer field1
@ExtendWith(Ext3)
Integer field2
void setupSpec(@ExtendWith(Ext4) Integer arg) {
// ...
}
// same for setup method
@ExtendWith(Ext5)
def "Sample test"(@ExtendWith(Ext6) Integer arg) {
// ...
}
}
All these @ExtendWith
would be found and registered.
Same as in junit, custom annotations could be used instead of direct @ExtendWith
:
@Target([ElementType.TYPE, ElementType.METHOD])
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith([Ext0, Ext1])
@interface MyExt {}
@MyExt
class Test extends Specification {
// ...
}
Programmatic registration is also te same:
class Test extends Specification {
@RegisterExtension
static Ext1 ext = new Ext1()
@RegisterExtension
Ext2 ext2 = new Ext2()
}
NOTE: @Shared
spock fields are not supported
As in junit, ParameterResolver
extensions could inject parameters into fixture and test method arguments (constructor is not supported because spock
does not allow constructors usage):
@ExtendWith(ParameterExtension)
class Test extends Specification {
void setupSpec(Integer arg) { ... }
void cleanupSpec(Integer arg) { ... }
void setup(Integer arg) { ... }
void cleanup(Integer arg) { ... }
def "Sample test"(Integer arg) { ... }
}
If extension implemented like this:
public class ParameterExtension implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) throws ParameterResolutionException {
return parameterContext.getParameter().getType().equals(Integer.class);
}
@Override
public Object resolveParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) throws ParameterResolutionException {
return 11;
}
}
In all spock methods argument would be set to 11.
Parameter resolution would be executed only on arguments marked as MethodInfo.MISSING_ARGUMENT
,
so data-iteration usage
would not be a problem:
class Test extends Specification {
@ExtendWith(ParameterExtension.class)
def "Sample test"(int a, Integer arg) {
when:
printn("iteration arg $a, junit arg $arg");
then:
true
where:
a | _
1 | _
2 | _
}
}
Here only arg
parameter would be resolved with junit extension.
NOTE: if junit extension will not be able to resolve parameter - it will remain as MethodInfo.MISSING_ARGUMENT
and subsequent spock extension could insert correct value (junit extensions not own arguments processing).
Supported:
- ExecutionCondition
- BeforeAllCallback
- AfterAllCallback
- BeforeEachCallback
- AfterEachCallback
- BeforeTestExecutionCallback
- AfterTestExecutionCallback
- ParameterResolver
- TestInstancePostProcessor
- TestInstancePreDestroyCallback
- TestExecutionExceptionHandler
Not supported:
- TestTemplateInvocationContextProvider - junit specific feature (no need for support)
- TestInstanceFactory - impossible to support because spock does not delegate test instance creation
- LifecycleMethodExecutionExceptionHandler - could be supported, but it is very specific
- InvocationInterceptor - same (very specific)
- TestWatcher - no need in context of spock
Of course, constructor parameters injection is not supported because spock does not allow spec constructors.
Only spock and spock-junit5 dependencies would be required:
testImplementation 'org.spockframework:spock-core:2.3-groovy-4.0'
testImplementation 'ru.vyarus:spock-junit5:1.2.0'
Note that spock-spring module is not required for spring-boot tests!
Now use spring junit extensions the same way as in raw junit
Example MVC test (based on this example):
@SpringBootTest
@AutoConfigureMockMvc
class ControllerTest extends Specification {
@Autowired
private MockMvc mvc
def "Test welcome ok"() {
mvc.perform(MockMvcRequestBuilders.get("/").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().string(equalTo("Hello World, Spring Boot!")))
expect:
// ofc. it's a bad spock test, but just to show that extensions work the same way
true
}
}
Example JPA test (based on this example):
@DataJpaTest
class BootTest extends Specification {
@Autowired
TestEntityManager testEM
@Autowired
BookRepository bookRepository
void setup() {
bookRepository.deleteAll()
bookRepository.flush()
testEM.clear()
}
def "Test save"() {
when:
Book b1 = new Book("Book A", BigDecimal.valueOf(9.99), LocalDate.of(2023, 8, 31))
bookRepository.save(b1)
Long savedBookID = b1.getId()
Book book = bookRepository.findById(savedBookID).orElseThrow()
then:
savedBookID == book.getId()
"Book A" == book.getTitle()
BigDecimal.valueOf(9.99) == book.getPrice()
LocalDate.of(2023, 8, 31) == book.getPublishDate()
}
}
Junit extensions would not be able to initialize @Shared
fields. So just don't use @Shared
on fields that must be initialized by junit extensions.
The reason is: shared fields are managed on a special test instance, different from instance used for test execution. But in junit lifecycle beforeEach could be called only once (otherwise extensions may work incorrectly) and so it is called only with actual test instance.
Even if junit extension initialize @Shared
field - it would make no effect because
it would be done on test instance instead of shared test instance and
on field access spock will return shared instance field value (not initialized - most likely, null).
This limitation should not be a problem.
Junit extensions support is implemented as global spock extension and so it would be executed before any other annotation-driven spock extension.
The following code shows all possible fixture methods and all possible interceptors available for extension (from spock docs)
@ExtendWith(JunitExtension)
@SpockExtension
class SpockLifecyclesOrder extends Specification {
// fixture methods
void setupSpec() { ... }
void cleanupSpec() { ... }
void setup() { ... }
void cleanup() { ... }
// feature
def "Sample test"() { ... }
}
@Retention(RetentionPolicy.RUNTIME)
@Target([ElementType.TYPE, ElementType.METHOD, ElementType.FIELD])
@ExtensionAnnotation(SpockExtensionImpl)
@interface SpockExtension {
String value() default "";
}
class SpockExtensionImpl implements IAnnotationDrivenExtension<SpockExtension> {
@Override
void visitSpecAnnotation(SpockExtension annotation, SpecInfo spec) {
spec.addSharedInitializerInterceptor new I('shared initializer')
spec.sharedInitializerMethod?.addInterceptor new I('shared initializer method')
spec.addInterceptor new I('specification')
spec.addSetupSpecInterceptor new I('setup spec')
spec.setupSpecMethods*.addInterceptor new I('setup spec method')
spec.allFeatures*.addInterceptor new I('feature')
spec.addInitializerInterceptor new I('initializer')
spec.initializerMethod?.addInterceptor new I('initializer method')
spec.allFeatures*.addIterationInterceptor new I('iteration')
spec.addSetupInterceptor new I('setup')
spec.setupMethods*.addInterceptor new I('setup method')
spec.allFeatures*.featureMethod*.addInterceptor new I('feature method')
spec.addCleanupInterceptor new I('cleanup')
spec.cleanupMethods*.addInterceptor new I('cleanup method')
spec.addCleanupSpecInterceptor new I('cleanup spec')
spec.cleanupSpecMethods*.addInterceptor new I('cleanup spec method')
spec.allFixtureMethods*.addInterceptor new I('fixture method')
}
static class I implements IMethodInterceptor { ... }
}
Junit (context type) | Spock | Kind | Registration |
---|---|---|---|
annotation extensions init | IAnnotationDrivenExtension (all methods) | ||
shared initializer | SHARED_INITIALIZER | spec.addSharedInitializerInterceptor | |
specification | SPEC_EXECUTION | spec.addInterceptor | |
BeforeAllCallback (c) | |||
setup spec | SETUP_SPEC | spec.addSetupSpecInterceptor | |
setup spec method | SETUP_SPEC | spec.setupSpecMethods*.addInterceptor | |
fixture method | SETUP_SPEC | spec.allFixtureMethods*.addInterceptor | |
TEST setupSpec | |||
initializer | INITIALIZER | spec.addInitializerInterceptor | |
TestInstancePostProcessor (c) | |||
feature | FEATURE_EXECUTION | spec.allFeatures*.addInterceptor | |
iteration | ITERATION_EXECUTION | spec.allFeatures*.addIterationInterceptor | |
BeforeEachCallback (m) | |||
setup | SETUP | spec.addSetupInterceptor | |
setup method | SETUP | spec.setupMethods*.addInterceptor | |
fixture method | SETUP | spec.allFixtureMethods*.addInterceptor | |
TEST setup | |||
BeforeTestExecutionCallback (m) | |||
feature method | FEATURE | spec.allFeatures*.featureMethod*.addInterceptor | |
TEST body | |||
AfterTestExecutionCallback (m) | |||
cleanup | CLEANUP | spec.addCleanupInterceptor | |
cleanup method | CLEANUP | spec.cleanupMethods*.addInterceptor | |
fixture method | CLEANUP | spec.allFixtureMethods*.addInterceptor | |
TEST cleanup | |||
AfterEachCallback (m) | |||
TestInstancePreDestroyCallback (m) | |||
cleanup spec | CLEANUP_SPEC | spec.addCleanupSpecInterceptor | |
cleanup spec method | CLEANUP_SPEC | spec.cleanupSpecMethods*.addInterceptor | |
fixture method | CLEANUP_SPEC | spec.allFixtureMethods*.addInterceptor | |
TEST cleanupSpec | |||
AfterAllCallback (c) |
Kind is a IMethodInvocation.getMethod().getKind()
. It is shown in case if you use AbstractMethodInterceptor
which uses kind for method dispatching.
Junit extensions postfix means: (c) - class context, (m) - method context
ParameterResolver
not shown because it's called just before method execution.
ExecutionCondition
and TestExecutionExceptionHandler
are also out of usual lifecycle.
Library use simplified junit contexts hierarchy:
- Global (engine) context
- Class context (created for each test class)
- Method context (created for each test method or data iteration)
In junit there are other possible contexts (for some specific features), but they are nto useful in context of spock.
Global context created once for all tests. It might be used as a global data store:
// BeforeAllCallback
public void beforeAll(ExtensionContext context) throws Exception {
// global storage (same for all tests)
Store store = context.getRoot().getStore(Namespace.create("ext"));
if(store.get("some") == null) {
// would be called only in first test with this extension
// (for other tests value would be preserved)
store.put("some", "val");
}
}
NOTE: this works exactly the same as it works in junit jupiter (showed just to confirm this ability).
Spock extension could access junit value storage with:
JunitExtensionSupport.getStore(SpecInfo, Namespace)
- obtain spec-level storeJunitExtensionSupport.getStore(IMethodInvocation, Namespace)
- obtain method or spec level store (depends on hook)
The second method is universal - always providing the most actual context (method, if available):
IMethodInterceptor interceptor = { invocation ->
Store store = JunitExtensionSupport.getStore(invocation, ExtensionContext.Namespace.create('name'))
}
The problem may only appear if you'll need to modify value stored on class level (in this case use
JunitExtensionSupport.getStore(invocation.getSpec(), ExtensionContext.Namespace.create('name'))
to get class level storage
(common for all methods in test class)).
Complete usage example:
@Retention(RetentionPolicy.RUNTIME)
@Target([ElementType.TYPE, ElementType.METHOD])
@ExtensionAnnotation(StoreAwareExtension)
@interface StoreAware {
String value() default "";
}
class StoreAwareExtension implements IAnnotationDrivenExtension<SpockStore> {
@Override
void visitSpecAnnotation(SpockStore annotation, SpecInfo spec) {
ExtLifecycle ls = new ExtLifecycle()
// listening for setup spec phase and test method execution
spec.addSetupSpecInterceptor ls
spec.allFeatures*.featureMethod*.addInterceptor ls
// create store for extension values on class level
Store store = JunitExtensionSupport.getStore(spec, ExtensionContext.Namespace.create(StoreAwareExtension.name))
// and store annotation value there
store.put('val', annotation.value())
}
}
class ExtLifecycle extends AbstractMethodInterceptor {
@Override
void interceptSetupSpecMethod(IMethodInvocation invocation) throws Throwable {
// access stored value
Object value = JunitExtensionSupport.getStore(invocation, ExtensionContext.Namespace.create(StoreAwareExtension.name)).get('val')
// do something
}
@Override
void interceptFeatureMethod(final IMethodInvocation invocation) throws Throwable {
// same store access here
}
}