Writing End-to-End Tests With Spock Framework – Configuration (Spring Edition)
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 thesetup()
method is run. - Inject the created
WebDriver
object into our specification object before thesetup()
method is run. We want to do this because quite often we want to use theWebDriver
object in thesetup()
method. For example, we might want to create a page object that is used by our feature methods and pass aWebDriver
object as a constructor argument. - Quit the
WebDriver
instance and close all browser windows before thecleanupSpec()
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 theWebDriver
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:
- 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. - Annotate the created annotation with the
@ExtensionAnnotation
annotation and specify the type of the loaded extension (SeleniumWebDriverExtension.class
). - Add an attribute called
driver
to the@SeleniumTest
annotation. This attribute configures the type of the createdWebDriver
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 thesetup()
method is invoked. We store the created object in a shared instance field because creating newWebDriver
objects is quite “expensive” and using a shared instance field ensures that we have to create only oneWebDriver
object per specification class. - We have to create an interceptor that quits the created
WebDriver
instance before thecleanupSpec()
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:
- 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 notWebDriver
, throw anInvalidSpecException
. - Create a new
WebDriver
object. - Inject the created
WebDriver
object into the shared instance field. - 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:
- Get the used
WebDriver
object. - If the
WebDriver
object was found, quit the foundWebDriver
instance and close all browser windows. - 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:
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:
- Create a new
SeleniumWebDriverInitializer
object. Because we want to inject the createdWebDriver
object into a shared instance field, we have to register this interceptor as a shared initializer interceptor. - Create a new
SeleniumWebDriverCleanUp
object. Because we want to quit the usedWebDriver
instance before thecleanupSpec()
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:
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:
- Create a new specification class and ensure that its feature methods are run when we run our end-to-end tests.
- Annotate our specification class with the
@SeleniumTest
annotation. When we do this, we can configure the type of the usedWebDriver
object by setting the value of the@SeleniumTest
annotation’sdriver
attribute. - 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.
Previous LessonNext Lesson