This article was first published on Medium.
A frequently asked question about Cucumber-JVM is how to share state between steps. This post describes how to use Spring with Cucumber to help you share objects between steps, without accidentally sharing state between scenarios (a common cause of flaky scenarios).
First of all, why not use static variables?
A solution often seen to share variables or objects between steps, is to declare static variables at the top of the step definitions file. However, there are several downsides this approach:
- As your project grows, you’ll probably want to divide your step definitions into different classes, divided into meaningful groups. At that point, you can no longer use the static variables declared in one step definition file.
- Using static variables might cause your tests to accidentally leak state into other scenarios, making them unreliable. Test automation is only any good if it is reliable.
Using Dependency Injection
The recommended solution to share state is to use Dependency Injection (DI). Cucumber supports several DI frameworks, including PicoContainer, Spring and Guice.
Using Dependency Injection with Cucumber will bring you several benefits:
- You can safely share state between your steps
- It will help improve the design of your test code; making it easier to share state between different step definition classes (so you can split them into meaningful groups).
- It will create and manage instances of classes for you.
Choosing a Dependency Injection framework
If your project already uses Dependency Injection, you could use the framework you already have. If not, consider using PicoContainer; it’s very light-weight and easy to use (at least, this is my personal experience in side projects).
Since my team already uses Spring in our projects, we’ve opted to use Spring with Cucumber.
Adding the cucumber-spring dependency
In order to use Spring with Cucumber, we need to add a cucumber-spring
dependency to our pom.xml
:
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-spring</artifactId>
<version>2.4.0</version>
<scope>test</scope>
</dependency>
Other dependencies
If you are using Spring and Cucumber in your project, you will already have those dependencies. Otherwise, you should add those as well.
Annotation based configuration
If you are using annotation based configuration, you can register your classes as Beans to be managed by Spring:
- Add the
@Component
annotation to each of the classes you’d like Spring to manage for you. - Add an annotation to specify the scope: the
@Scope("cucumber-glue")
, in order to have cucumber-spring remove them after each scenario! - Add the location of your classes to the
@ComponentScan
of your (test) configuration.
import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @Configuration @ComponentScan("your.package") public class Config { }
This approach works for any classes that you have defined yourself. This may include:
- domain objects (like a “Customer” or an “Account”)
- any other step definition files (if you have split your files)
- any “services” or “helper” classes you extract to represent parts of the system your tests interact with (like PageObjects, REST API’s, etc.). The steps can then call these services/helpers, instead of each other.
If you are using classes that you have not defined yourself, you cannot annotate them as Components for Spring to find with the component scan. You will have to explicitly register them as Beans in your (test) configuration.
Here is an example, using Selenium WebDriver:
import org.openqa.selenium.WebDriver; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; @Configuration @ComponentScan("your.package") public class Config { @Bean @Scope("cucumber-glue") public WebDriver webDriver() { // return a driver with desired capabilities } }
In our latest projects, we are using SpringBoot. We can annotate our StepDefinitions class with @SpringBootTest(classes = [Config.class]);
.
XML configuration
Alternatively, you can specify your beans in a .cucumber.xml
file, like this. Here is an example (again using Selenium WebDriver; this could also be one of your own classes):
<bean class="org.openqa.selenium.WebDriver" scope="cucumber-glue" />
You can annotate your StepDefinitions with @ContextConfiguration("classpath:cucumber.xml")
.
Using the Beans
You can now use the Beans, by autowiring them where you need them.
For example, you can Autowire your WebDriver to your PageObject:
import org.openqa.selenium.WebDriver; import org.springframework.beans.factory.annotation.Autowired; public class PageObject { @Autowired WebDriver driver; // the rest of your page object }
Conclusion
As you can see, it is relatively easy to add Cucumber-Spring to manage state. You are now able to safely share state between your steps, as well as split your step definition files into meaningful groups.
This will make your project easier to understand and maintain, and your tests more reliable.