Skip to content

xvik/spock-junit5

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

spock-junit5

License CI Appveyor build status codecov

About

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.

Motivation

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.

Setup

Maven Central

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'

Compatibility

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
Snapshots

You can use snapshot versions through JitPack:

  • Go to JitPack project page
  • Select Commits section and click Get 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)

Usage

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.

Custom annotations

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

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

Method parameters

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).

What is supported

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.

Usage with Spring-Boot

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()
    }
}

Spock @Shared state

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.

Lifecycle

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.

Contexts hierarchy

Library use simplified junit contexts hierarchy:

  1. Global (engine) context
  2. Class context (created for each test class)
  3. 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).

Access storage from spock

Spock extension could access junit value storage with:

  • JunitExtensionSupport.getStore(SpecInfo, Namespace) - obtain spec-level store
  • JunitExtensionSupport.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
    }
}

java lib generator