Test With Spring Course
Save Time by Writing Less Test Code
  • Home
  • Pre-Sales FAQ
  • Support
  • Log In

/ December 28, 2017 / petrikainulainen

Writing End-to-End Tests With Spock Framework – Configuration (Spring Edition)

Lesson Progress:
← Back to Topic

After we have finished this lesson, we:

  • Can create a custom Spock extension that configures Selenium WebDriver before our feature methods are run and frees the reserved resources after our feature methods have been run.
  • Understand how we can configure our end-to-end tests by using our custom extension.

This lesson assumes that:

  • You can run your end-to-end tests by using either Maven or Gradle
  • You are familiar with Selenium WebDriver
  • You can configure your end-to-end tests when you are using JUnit
  • You are familiar with Spock Framework

Watch the Lesson

 

The text version of this lesson is given the following:

Creating a Custom Spock Extension

Before we can write end-to-end tests with Selenium WebDriver, we have to figure out a way to:

  • Create and configure a new WebDriver object before the setup() method is run.
  • Inject the created WebDriver object into our specification object before the setup() method is run. We want to do this because quite often we want to use the WebDriver object in the setup() method. For example, we might want to create a page object that is used by our feature methods and pass a WebDriver object as a constructor argument.
  • Quit the WebDriver instance and close all browser windows before the cleanupSpec() method is run. This allows us to free the reserved resources after all feature methods have been run and we can “clean” the state of the WebDriver object after a feature method has been run. For example, we might want to log a user out after a feature method has been run.
  • Minimize the negative performance effects caused to our test suite.

Spock has a versatile support for extending its behavior. If we want to add custom behavior to the Spock lifecycle, we have to implement a custom extension. Spock supports these two extension types:

  • A global extension is loaded and used automatically when Spock is run.
  • An annotation driven local extension is loaded and used when a specification class is annotated with a special annotation that registers the loaded extension.

During this lesson we will create an annotation driven local extension because of these three reasons:

  • We can configure our extension by adding attributes to the annotation that registers the loaded extension.
  • The marker annotation allows us to document the behavior that is added to the Spock lifecycle. I think that this is a clear advantage over global extensions that just add “invisible” magic to the Spock lifecycle.
  • An annotation driven local extension allows us to choose when the extension is loaded (and used). In other words, we can minimize the performance effects caused by our extension because we can load it only when it’s required.

Next, we will find out how we can create the required marker annotations.

Creating the Marker Annotations

We can create the required marker annotations by following these steps:

First, we have to create the annotation that registers our custom Spock extension by following these steps:

  1. Create an annotation called @SeleniumTest that can be added to a specification class and ensure that the created annotation can be read at runtime by using reflection.
  2. Annotate the created annotation with the @ExtensionAnnotation annotation and specify the type of the loaded extension (SeleniumWebDriverExtension.class).
  3. Add an attribute called driver to the @SeleniumTest annotation. This attribute configures the type of the created WebDriver object, and its default value is: ChromeDriver.class.

The source code of the @SeleniumTest annotation looks as follows:

import org.openqa.selenium.WebDriver
import org.openqa.selenium.chrome.ChromeDriver
import org.spockframework.runtime.extension.ExtensionAnnotation

import java.lang.annotation.Documented
import java.lang.annotation.ElementType
import java.lang.annotation.Inherited
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target

@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@ExtensionAnnotation(SeleniumWebDriverExtension.class)
@interface SeleniumTest {

    Class<? extends WebDriver> driver() default ChromeDriver.class
}

I admit that this annotation is not nearly as useful as it could be. I decided to keep it as simple as possible because I have no idea what kind of configuration options you want to use. That being said, you can add the missing configuration options to this annotation and configure the created WebDriver object by following the instructions given in this lesson.

Credits:

I got this idea from a blog post titled: Spring Boot Integration Testing with Selenium by Rafał Borowiec.

Second, we have to create an annotation called @SeleniumWebDriver that identifies the field in which the created WebDriver object should be injected. This annotation can be added to a field of a specification class.

The source code of the @SeleniumWebDriver annotation looks as follows:

import java.lang.annotation.*

@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface SeleniumWebDriver {

}

Let’s move on and find out how we can create a new WebDriver object, inject this object into our Spock specification, and quit the used WebDriver instance.

Creating the Required Interceptors

Spock extensions can add custom behavior to the Spock lifecycle by attaching custom interceptors to various interception points. Each interception point can have multiple interceptors, and a single interceptor must not make any assumptions about the invocation order of the interceptors attached to the same interception point.

We have to create two custom interceptors:

  • We need an interceptor that creates a new WebDriver object and injects the created object into a shared instance field. This interceptor must be run before the setup() method is invoked. We store the created object in a shared instance field because creating new WebDriver objects is quite “expensive” and using a shared instance field ensures that we have to create only one WebDriver object per specification class.
  • We have to create an interceptor that quits the created WebDriver instance before the cleanupSpec() method is run.

Next, we will find out how we can create an interceptor that creates a new WebDriver object and injects this object into our specification object.

Creating a New WebDriver Object

When we want to create the interceptor that creates a new WebDriver object and injects this object into our specification object, we have to follow these steps:

First, because we want to create an interceptor that can be attached to only one interception point, we have to create a new class that implements the IMethodInterceptor interface.

After we have created this class, its source code looks as follows:

import org.spockframework.runtime.extension.IMethodInterceptor

class SeleniumWebDriverInitializer implements IMethodInterceptor {

}

If we want to create an interceptor that can be attached to multiple interception points, we have to extend the AbstractMethodInterceptor class. It provides several methods that are invoked at different interception points. If we want to add custom behavior to a specific interception point, we have to simply override the method that is invoked at that interception point.

Second, we have to add a new field called: webDriverClass to our interceptor class and set the value of this field by using constructor injection.

After we have added a new field to our interceptor class, the source code of our interceptor class looks as follows:

import org.openqa.selenium.WebDriver
import org.spockframework.runtime.extension.IMethodInterceptor

class SeleniumWebDriverInitializer implements IMethodInterceptor {

    private final Class<? extends WebDriver> webDriverClass

    protected SeleniumWebDriverInitializer(Class<? extends WebDriver> webDriverClass) {
        this.webDriverClass = webDriverClass
    }
}

Third, we have to implement the intercept() method of the IMethodInterceptor interface by following these steps:

  1. Get the FieldInfo object which represents the shared instance field that is annotated with the @SeleniumWebDriver annotation. If a unique shared instance field is not found or the type of the found field is not WebDriver, throw an InvalidSpecException.
  2. Create a new WebDriver object.
  3. Inject the created WebDriver object into the shared instance field.
  4. Ensure that Spock continues the execution of its lifecycle.

After we have implemented the intercept() method, the source code of our interceptor class looks as follows:

import org.openqa.selenium.WebDriver
import org.spockframework.runtime.InvalidSpecException
import org.spockframework.runtime.extension.IMethodInterceptor
import org.spockframework.runtime.extension.IMethodInvocation
import org.spockframework.runtime.model.FieldInfo
import org.spockframework.runtime.model.SpecInfo
import spock.lang.Shared

class SeleniumWebDriverInitializer implements IMethodInterceptor {

    private final Class<? extends WebDriver> webDriverClass

    protected SeleniumWebDriverInitializer(Class<? extends WebDriver> webDriverClass) {
        this.webDriverClass = webDriverClass
    }

    @Override
    void intercept(IMethodInvocation invocation) throws Throwable {
        def webDriverField = getWebDriverField(invocation.getSpec())
        def webDriver = createWebDriver()
        webDriverField.writeValue(invocation.getInstance(), webDriver)
        invocation.proceed()
    }

    private static FieldInfo getWebDriverField(SpecInfo spec) {
        def fields = spec.getAllFields()
        def webDriverFields = fields.findAll(
                { it.isAnnotationPresent(SeleniumWebDriver.class) }
        )

        if (webDriverFields.isEmpty()) {
            throw new InvalidSpecException(
                    'Cannot initialize specification class because ' +
                            'it has no field that is annotated with the ' +
                            '@SeleniumWebDriver annotation'
            )
        }

        if (webDriverFields.size() > 1) {
            throw new InvalidSpecException(
                    'Cannot initialize specification class because ' +
                            'it has multiple fields that are annotated with ' +
                            'the @SeleniumWebDriver annotation'
            )
        }

        def webDriverField = webDriverFields[0]
        if (!webDriverField.isAnnotationPresent(Shared.class)) {
            throw new InvalidSpecException(
                    'Cannot initialize specification class because ' +
                            'the WebDriver field is not shared'
            )
        }

        if (!webDriverField.getType().equals(WebDriver.class)) {
            throw new InvalidSpecException(
                    'Cannot initialize specification class because ' +
                            'the type of the field that is annotated with the ' +
                            '@SeleniumWebDriver annotation is not WebDriver'
            )
        }

        return webDriverField
    }

    private WebDriver createWebDriver() {
        try {
            return webDriverClass.newInstance()
        } catch (InstantiationException e) {
            throw new RuntimeException(String.format(
                    'Cannot instantiate WebDriver. Is %s a non abstract ' +
                            'class that has a no argument constructor?',
                    webDriverClass.getCanonicalName()
            ))
        } catch (IllegalAccessException e) {
            throw new RuntimeException(String.format(
                    'Cannot instantiate WebDriver. Does %s have a ' +
                            'public constructor?',
                    webDriverClass.getCanonicalName()
            ))
        }
    }
}

Let’s move on and find out how we can create an interceptor that quits the used WebDriver instance.

Quitting the Used WebDriver Instance

We can create the interceptor that quits the used WebDriver instance by following these steps:

First, because we want to create an interceptor that can be attached to only one interception point, we have to create a new class that implements the IMethodInterceptor interface.

After we have created this class, its source code looks as follows:

import org.spockframework.runtime.extension.IMethodInterceptor
 
class SeleniumWebDriverCleanUp implements IMethodInterceptor {

}

Second, we have to implement the intercept() method of the IMethodInterceptor interface by following these steps:

  1. Get the used WebDriver object.
  2. If the WebDriver object was found, quit the found WebDriver instance and close all browser windows.
  3. Ensure that Spock continues the execution of its lifecycle.

After we have implemented the intercept() method, the source code of the SeleniumWebDriverCleanUp class looks as follows:

import org.openqa.selenium.WebDriver
import org.spockframework.runtime.extension.IMethodInterceptor
import org.spockframework.runtime.extension.IMethodInvocation

class SeleniumWebDriverCleanUp implements IMethodInterceptor {

    @Override
    void intercept(IMethodInvocation invocation) throws Throwable {
        def webDriver = getWebDriver(invocation)
        webDriver?.quit()
        invocation.proceed()
    }

    private static WebDriver getWebDriver(IMethodInvocation invocation) {
        def webDriverField = invocation.getSpec()
                .getAllFields()
                .find({ it.isAnnotationPresent(SeleniumWebDriver.class)} )

        return webDriverField.readValue(invocation.getInstance())
    }
}

Additional Reading:

  • Spock Framework Reference Documentation – Extensions: Interceptors

We have now implemented the required interceptors. Next, we will find out how we can create a custom Spock extension that registers our custom interceptors.

Creating Our Custom Spock Extension

We can create an annotation driven local extension by following these steps:

First, we have to create a new class that extends the AbstractAnnotationDrivenExtension class. When we extend that class, we have to provide one type parameter that specifies the type of the annotation that registers our custom extension (SeleniumTest).

After we have created our extension class, its source code looks as follows:

import org.spockframework.runtime.extension.AbstractAnnotationDrivenExtension
import org.spockframework.runtime.model.SpecInfo

class SeleniumWebDriverExtension 
		extends AbstractAnnotationDrivenExtension<SeleniumTest> {
}

We can also implement the IAnnotationDrivenExtension interface. That being said, I like to extend the AbstractAnnotationDrivenExtension class because it provides empty implementations for the methods declared by the IAnnotationDrivenExtension interface. In other words, I don’t have to implement all five methods. I can simply override the methods I need.

Second, because our annotation must be added to a specification class, we have to override the visitSpecAnnotation() method of the AbstractAnnotationDrivenExtension class.

After we have overriden this method, the source code of our extension class looks as follows:

import org.spockframework.runtime.extension.AbstractAnnotationDrivenExtension
import org.spockframework.runtime.model.SpecInfo

class SeleniumWebDriverExtension 
		extends AbstractAnnotationDrivenExtension<SeleniumTest> {

    @Override
    void visitSpecAnnotation(SeleniumTest annotation, SpecInfo spec) {

    }
}

Third, we have to implement the visitSpecAnnotation() method by following these steps:

  1. Create a new SeleniumWebDriverInitializer object. Because we want to inject the created WebDriver object into a shared instance field, we have to register this interceptor as a shared initializer interceptor.
  2. Create a new SeleniumWebDriverCleanUp object. Because we want to quit the used WebDriver instance before the cleanupSpec() method is run, we have to register this interceptor as a cleanup spec interceptor.

After we have implemented the visitSpecAnnotation() method, the source code of our extension class looks as follows:

import org.spockframework.runtime.extension.AbstractAnnotationDrivenExtension
import org.spockframework.runtime.model.SpecInfo

class SeleniumWebDriverExtension 
		extends AbstractAnnotationDrivenExtension<SeleniumTest> {

    @Override
    void visitSpecAnnotation(SeleniumTest annotation, SpecInfo spec) {
        spec.addSharedInitializerInterceptor(
                new SeleniumWebDriverInitializer(annotation.driver())
        )
        spec.addCleanupSpecInterceptor(new SeleniumWebDriverCleanUp())
    }
}

Additional Reading:

  • Spock Framework Reference Documentation – Extensions: Writing Custom Extensions

We have now created a custom Spock extension that creates a new WebDriver object, injects the created object into our Spock specification, and quits the used WebDriver instance. Let’s move on and find out how we can use this extension when we write end-to-end tests.

Using Our Custom Spock Extension

We can use our custom Spock extension by following these steps:

  1. Create a new specification class and ensure that its feature methods are run when we run our end-to-end tests.
  2. Annotate our specification class with the @SeleniumTest annotation. When we do this, we can configure the type of the used WebDriver object by setting the value of the @SeleniumTest annotation’s driver attribute.
  3. Add a shared instance field to our specification class and annotate this field with the @SeleniumWebDriver annotation.

The source code of our specification class looks as follows:

import org.junit.experimental.categories.Category
import org.openqa.selenium.WebDriver
import org.openqa.selenium.chrome.ChromeDriver
import spock.lang.Shared
import spock.lang.Specification

@SeleniumTest(driver = ChromeDriver.class)
@Category(EndToEndTest.class)
class ExampleSpec extends Specification {

    @SeleniumWebDriver
    @Shared WebDriver browser
}

We can now create a custom Spock extension that configures our end-to-end tests and use this extension when we write end-to-end tests. Let’s summarize what we learned from this lesson.

Summary

This lesson has taught us five things:

  • We can add custom behavior to the Spock lifecycle by creating custom extensions.
  • The annotation driven local extensions allow us to document the behavior that is added to the Spock lifecycle.
  • Spock extensions can add custom behavior to the Spock lifecycle by attaching custom interceptors to various interception points.
  • We must create an interceptor that creates a new WebDriver object and injects this object into a shared instance field, and register this interceptor as a shared initializer interceptor.
  • We must create an interceptor that quits the used WebDriver instance and register this interceptor as a cleanup spec interceptor.

Get the source code from Github

← Previous Lesson Next Lesson →

Can I help you?

This is a free sample lesson of my Test With Spring course. If this lesson helped you to solve your problem, you should find out how my testing course can help you.

Support and Privacy

  • Pre-Sales FAQ
  • Support
  • Cookie Policy
  • No Bullshit Privacy Policy
  • No Bullshit Terms and Conditions

Test With Spring Course

  • Starter Package
  • Intermediate Package
  • Master Package

Free Sample Lessons

  • Introduction to JUnit 4
  • Introduction to Unit Testing
  • Introduction to Integration Testing
  • Introduction to End-to-End Testing
  • Introduction to Spock Framework
  • Introduction to Integration Testing – Spock Edition
  • Writing End-to-End Tests With Spock Framework

Copyright Koodikupla Oy 2016 — Built on Thesis by Themedy