Setup Once Per Feature With Spock Extensions

Disclaimer: This example is a hack that uses un-official/non-public Spock APIs.

A proper implementation of this extensions is also included and distributed as part of my Spock Extensions module; The library can be downloaded from here and is also hosted on github.

Also, as seen on Stephen Chin’s Nighthacker :)

The Missing pieces

Prior to discovering the awesome Spock Framework, I used Test-NG for all my testing needs. Although I am now completely hooked on Spock there is one thing I miss in Test-NG , and that’s the ability to run a one-time setup procedure before the test method, regardless of iterations.

I’ll try to clarify the scenario.
Let’s take a simple test specification:

class SimpleSpec extends Specification {

    def setupSpec() {
        println 'Performing long, class wide setup'
    }

    def 'Some spec'() {
        setup:
        println 'Performing long, spec wide setup'

        expect:
        true == true
        println 'Performing assertion'
    }
}

The above specification will produce the following output:

Performing long, class wide setup
Performing long, spec wide setup
Performing assertion

So as expected, the specification will execute the long setup procedure in setupSpec, execute the long setup procedure in the spec’s setup block and then execute the assertions.

Now let’s add several iterations to our spec:

def 'Some spec'() {
        setup:
        println 'Performing long, spec wide setup'

        expect:
        true == true
        println "Performing assertion $x"

        where:
        x << [1,2,3]
}

The above specification will produce the following output:

Performing long, class wide setup
Performing long, spec wide setup
Performing assertion 1
Performing long, spec wide setup
Performing assertion 2
Performing long, spec wide setup
Performing assertion 3

In this case, we notice that the setup procedure in the spec’s setup block is executed before every iteration; In cases of long setup procedures, this can be a real waste of time and resources.

At the time of writing this blog post, Spock has no built-in way to deal with this issue. So, what can we do?

T3h Hax0rz

Good news, everyone! We can easily solve this issue using Spock’s annotation-driven extension points!
Because the API is not yet finalized, official documentation is practically nonexistent; luckily, we can rely on talented people such as Luke Daley and Evgeny Goldin to spill the beans.

Let’s draw up a plan:

  1. Create an extension annotation.
  2. Create an annotation-driven extension class.
  3. Create an event interceptor class.
The Extension Annotation

With the annotation we can select the methods that apply to our extension, specify the backing extension class and specify the name of the setup method we’d like to execute before the feature.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)

//Specify the extension class that backs this annotation
@org.spockframework.runtime.extension.ExtensionAnnotation(SetupOnceExtension)

public @interface SetupOnceExtensionAnnotation {

    //Accept a string value that represents the name of the setup method to execute
    String value();
}
The Extension Class

Bound to our annotation, the class extends AbstractAnnotationDrivenExtension, from which we inherit different levels of annotation visitation methods: Spec, Feature, Fixture and Field. Every visitation methods is also provided with mutable metadata descriptors according to the overridden level.
Because our main goal is to execute a setup method before a feature, it’s best we override the visitFeatureAnnotation; this also gives us a chance to meddle with FeatureInfo and use it to subscribe our event interceptor.

class SetupOnceExtension extends AbstractAnnotationDrivenExtension<SetupOnceExtensionAnnotation> {

    @Override
    void visitFeatureAnnotation(SetupOnceExtensionAnnotation annotation, FeatureInfo feature) {

        //Retrieve the name of the setup method we'd like to invoke from our annotation
        def methodToInvoke = annotation.value()

        //Construct and subscribe our event interceptor
        def interceptor = new SetupOnceInterceptor(methodToInvoke: methodToInvoke)
        feature.addInterceptor(interceptor)
    }
}
The Event Interceptor

Extending the AbstractMethodInterceptor we’re given many interception points, but the one that interests us most is interceptFeatureExecution.
This point is executed right before the feature so it gives us a chance to execute our setup method.

class SetupOnceInterceptor extends AbstractMethodInterceptor {

    String methodToInvoke

    @Override
    void interceptFeatureExecution(IMethodInvocation invocation) throws Throwable {

        //Get the instance of the executed spec
        def currentlyRunningSpec = invocation.sharedInstance

        //Execute the setup method we specified in the annotation value
        currentlyRunningSpec."$methodToInvoke"()
        invocation.proceed()
    }
}
Putting It All Together

Revisiting our first Specification, we can now annotate our feature method with the SetupOnceExtensionAnnotation, create a separate setup method (‘setupMethod’ in our example) and move the code from the feature’s setup block into it.

class SimpleSpec extends Specification {

    def setupSpec() {
        println 'Performing long, class wide setup'
    }

    @SetupOnceExtensionAnnotation('setupMethod')
    def 'Some spec'() {
        expect:
        true == true
        println "Performing assertion $x"

        where:
        x << [1,2,3]
    }

    void setupMethod() {
        println 'Performing long, spec wide setup'
    }
}

Now when executed, our specification will produce the following output:

Performing long, class wide setup
Performing long, spec wide setup
Performing assertion 1
Performing assertion 2
Performing assertion 3

Issue busted!

About these ads

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s