Using JUnit 5 extensions in Spock 2 tests
Spock 2 does not support JUnit 5 extensions out of the box:
We looked at adding a jupiter extension support module, but quickly dismissed it and decided to focus on finishing Spock 2.0, since we would have to emulate a large chunk of Jupiters internals to make it work.
It is a problem for two (obvious) reasons:
- JUnit is much more popular and there is a great chance to find existing JUnit 5 extension, but almost no chance to find a Spock one.
- For developer, maintaining two similar extensions for JUnit and Spock is a problem too: in JUnit 4 times Spock extensions model allows building much nicer extensions, but JUnit 5 abilities are almost equivalent.
I faced the later situation with dropwizard-guicey: currently it provides separate implementations for JUnit 4, JUnit 5 and Spock 1 and all guicey tests using Spock 1. But Spock 1 can't run on jdk16 or above which makes impossible for me to test guicey itself with jdk16 or 17.
Instead of implementing separate Spock 2 extensions, I decided to try implementing JUnit 5 extensions support and here it is: spock-junit5.
With it, alsmost any JUnit 5 extension could be used in Spock test (including custom annotations, parameters injection etc):
@ExtendWith(JunitExt)
class Test extends Specification {
def "Check something"() {
when: "do something"
...
then: "some condition is correct"
...
}
}
This post is not a user guide: you can always find all usage details in the project home page. Instead, It will highlight the motivation and some technical details.
Why Spock
If JUnit 5 is so much better then JUnit 4 maybe it's time to get rid of Spock? No, Spock is still way ahead of JUnit in terms of writing tests:
- Spock tests are more structured and self-descriptive
- Groovy allows writing much more compact and better-redable tests
- Spock errors reporting is wonderful
- Parametrized tests in Spock are compact and easy to read
Small example:
class TypesCompatibilityTest extends Specification {
def "Check types compatibility"() {
expect:
TypeUtils.isCompatible(type1, type2) == res
where:
type1 | type2 | res
String | Integer | true
Object | Integer | true
String | Object | true
}
}
And in case of error:
Condition not satisfied:
TypeUtils.isCompatible(type1, type2) == res
| | | | | |
| false | | | true
| | | false
| | class java.lang.Integer
| class java.lang.String
class ru.vyarus.java.generics.resolver.util.TypeUtils
Nice, isn't it? And no way to get anything near like this in JUnit.
Have to admit that sometimes (rare!) it cause additional problems: groovy compiler is not a java compiler and (in very! rare cases) there might be unexpected compilation problems with groovy classes (not tests, but additional classes, required for test). But you can always convert such classes to java (in IDEA there is an action for it) and everything will work. Besides, groovy 3 and 4 get much closer to java native compilation behaviour.
Using Spock (heavily) for years I have never faced cases I couldn't workaround.
Extension models
Spock and JUnit 5 extension models are both driven by annotations: you must put an annotation somewhere in test to activate extension (global extensions also possible, but almost never used).
But extension implementation approaches are different.
Spock extension
In Spock your extension must implement IAnnotationDrivenExtension
where you must override one of its methods. It is already a bit confusing because you need to implement methods based on annotation target (different for class, field and test method annotations).
Inside this method you can apply an interceptor. This is very flexible, as you can hook in almost any lifecycle stage, but requires some knowledge (what to intercept and how to register interceptor properly).
For example, suppose extension annotation would be used on test class and extension would intercept test method execution:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@ExtensionAnnotation(MyExtImpl)
@interface MyExt { }
class MyExtImpl implements IAnnotationDrivenExtension<MyExt> {
@Override
void visitSpecAnnotation(MyExt annotation, SpecInfo spec) {
spec.allFeatures*.featureMethod*.addInterceptor { invocation ->
// do something before test method
invocation.proceed()
// do something after test
}
}
}
Here interceptor implicitly implements IMethodInterceptor
(note that invocation.proceed()
is required).
But often it is required to hook into multiple places and here you can use AbstractMethodInterceptor
with pre-defined lifecycle methods. The only problem would be to properly register such intercepter in all required places (Spock docs highlight all possible places).
Same example, but extension hooks around test methods and setup test stage:
class MyExtImpl implements IAnnotationDrivenExtension<MyExt> {
@Override
void visitSpecAnnotation(MyExt annotation, SpecInfo spec) {
MyInterceptor interceptor = new MyIntrerceptor()
spec.addSetupInterceptor interceptor
spec.allFeatures*.featureMethod*.addInterceptor interceptor
}
}
class MyInterceptor extends AbstractMethodInterceptor {
@Override
void interceptSetupMethod(final IMethodInvocation invocation) {
// do something
invocation.proceed()
}
@Override
void interceptFeatureMethod(final IMethodInvocation invocation) {
// do something
invocation.proceed()
}
}
As you can see, very flexible, but not very easy to understand model. And, by the way, it might not be obvious, but Spock extensions does not require groovy and could be written in java (Spock itself is written in java).
(More details in Spock extensions guide)
Junit 5 extension
In JUnit 5 there is a set of extension interfaces:
- ExecutionCondition
- BeforeAllCallback
- AfterAllCallback
- BeforeEachCallback
- AfterEachCallback
- BeforeTestExecutionCallback
- AfterTestExecutionCallback
- ParameterResolver
- TestInstancePostProcessor
- TestInstancePreDestroyCallback
- TestExecutionExceptionHandler
- (and a few more)
All of them extends base Extension
class.
Actual extension implement required interfaces. And extension from Spock example would look like:
public class MyExtension implements BeforeEachCallback,
BeforeTestExecutionCallback,
AfterTestExecutionCallback {
@Override
public void beforeEach(ExtensionContext context) throws Exception {
// do something
}
@Override
public void beforeTestExecution(ExtensionContext context) throws Exception {
// do something
}
@Override
public void afterTestExecution(ExtensionContext context) throws Exception {
// do something
}
}
Extension activation:
@ExtenWith(MyExtensions.class)
public class MyTest { ... }
Or you can make a custom annotation:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(MyExtensions.class)
public @interface MyExt {}
(More details in JUnit extension guide)
As you can see, JUnit extensions are very easy to write and understand. They are a bit less powerful then Spock extensions, but, to be honest, I doubt it would be a problem in the majority of cases.
So JUnit extensions are ideal for writing test integrations.
Lifecycles compatibility
JUnit and Spock share pretty much the same lifecycle (I did a comparison table).
Differences:
- Spock owns test instance creation and so it is impossible to simulate JUnit test instance factory
- Spock does not allow constructors (and so parameter injection in constructor is impossible)
- Spock has a shared fields concept (more on it below)
In Spock, "shared fields" is a core feature, but it is implemented with an additional test instance: shared instance contains all shared fields and a new test instance created for each test method. When you accessing shared fields from test method it is "magically" routed to different (shared) instance.
There are two initialization hooks in Spock (each with different test instance):
specInfo.addSharedInitializerInterceptor
specInfo.addInitializerInterceptor
In JUnit, shared fields could be simulated with @TestInstance(LifeCycle.PER_CLASS). With it we will have a single test instance for all tests (by default, there are different instances for each test method).
JUnit extensions always receive a context object (as a parameter). It could be class-level context or method-level context containing test instance reference (there are other types, but in general these are the most important). If I would try to support shared fields, I'll have to call some JUnit extensions (like BeforeEach) two times, which may break extensions internal state. So it is not possible to support Spock shared fields for JUnit extensions (only one instance could be used in context).
Shared fields would be "invisible" for JUnit extensions: even if JUnit extension tried to initialize Spock shared field with reflection (it's just a field) - it would do it on "test" instance, but Spock magic would make this value invisible as every access to shared field is routed to a different instance.
Anyway, it should not be a problem because shared fields are not used often.
Copy paste
Here I should quote again Spock maintainers:
We looked at adding a jupiter extension support module, but quickly dismissed it and decided to focus on finishing Spock 2.0, since we would have to emulate a large chunk of Jupiters internals to make it work.
That is so true! I have to copy-paste a lot from junit-jupiter-engine (to grant the same behaviour).
Jupiter is a complete test engine and so it contains all the code required for test execution (fixture method calls, test method calls, exception handling etc.). As a result, it has some additional abstractions, which are not required in context of spock.
The root jupiter concept is descriptor (internal). There are multiple context levels: root with global extensions and configurations, test level, method level etc. Each descriptor contains (among other staff) extension resolution and execution) logic. For example, class level context - class level extensions resolution and before/after all calls. Method level context - method level extension resolution and before/after each calls (and others ofc.).
It was the most complex part: in context of spock, tests execution code is obviously not required and so only extension-related logic must be copy-pasted (and highly adapted) in order to preserve the same extensions behaviour. Extension registration logic was collected (from various places) into ExtensionUtils and all execution logic (extensions processing) into JupiterApiExecutor.
It was important to preserve the same extensions order and even exceptions behaviour (that's why copy-pasting was important).
As was already mentioned, JUnit extensions require context objects. JUnit use ~5 context implementations, but in terms of integration only 2 are important: class context and method context. It was also impossible to simply copy-paste existing jupiter implementations as they are too tied to internal contexts.
Contexts were re-implemented (they are quite simple). In context of spock many abilities are not used at all and so many context methods simply return empty objects.
Thankfully, there are shared utilities in JUnit which might be used without copy-pasting.
For example, annotations search logic could be re-used through org.junit.platform.commons.util.AnnotationUtils
class (not the official API, but also available).
And these missing parts were copied almost as-is:
- Value storage implementation (ExtensionValuesStore) - exact copy
- Condition evaluation (ConditionEvaluator) - exact copy
- ParameterContext implementation for parameter resolver extensions (DefaultParameterContext) - exact copy
- Extensions registry (ExtensionRegistry) - with simplifications because in JUnit there are extension tiers which are not needed in context of integration
Integration
JUnit extensions integration was implemented as global Spock extension. So you don't need to use any additional annotations to activate it: JUnit extensions registration will work exactly the same way as in JUnit.
Overall algorithm is pretty simple:
- Try to find class-level extensions for each test. Here class-level JUnit context is created (without test instance)
- Register required interceptors to be able to call JUnit extensions in correct Spock lifecycle stages (see this table for merged lifecycle)
- On test method initialization (when test instance created for test method) create method-level context (containing test instance reference)
I have to hardcode two lists of extension types: supported and not supported. If not supported extension appear I could warn user about it in log (but not crash because it might be not important and everything would work without it).
If no known extension types recognized in registered extension - execution would would fail because nothing could be done in this case: all extension types have to be supported manually (at exact point extension method must be called manually for all registered extensions) and so unknown extensions couldn't do anything.
Probably not very obvious is conditional JUnit extensions support: for example, JUnit @Disabled
could skip Spock test.
Before that project I was sure that Spock does not support parameter injections for test or fixture methods (never read guide that far), but it can! In fact, it keeps a marked list of parameters and all parameters Spock is not aware of are marked as MethodInfo.MISSING_ARGUMENT
. So all I need is to call JUnit extensions for unknown parameters.
Important moment here is to not throw error when parameter not matched (as JUnit did) because there might be other Spock extensions responsible for injection (which could execute after global extension).
Maturity
So the project is almost completely built on copy-pasted code from jupiter engine. Ok, it will work, for now. But what about the future, when JUnit evolve (and it will)?
First of all, there are a lot of comments refencing original code source. So it would be possible to track and apply changes.
Secondly, there are two sets of tests: first set validates JUnit extensions behavior (with jupiter engine) and the second one validates that Spock behaves the same. So if some JUnit behaviour will change - I will know it from the first group of tests and correct Spock behaviour accordingly.
Previously, I wrote separate articles about how these tests were implemented using jupiter TestKit: