Cucumber-JVM Sharing state using Spring Dependency Injection

Introduction

When all the steps of a scenario are defined in the same step definition class there is no issue with sharing object state between steps. But things get a bit murkier when the same steps are defined across multiple classes. Now the question becomes how to manage the sharing of object state. One could make use of static, but then the code will have to deal with resetting the state before each scenario, possibly in a before hook Not to mention possible issues if scenarios are executed in parallel.

The cleanest solution is to make use of Dependency Injection (DI). Cucumber JVM currently supports integration with DI containers like Spring, Pico, Guice, CDI2, Weld, Needle. This article deals with using Spring container to manage state.

The source code can be found at this location. The cucumber version used is 6.11.0. The spring version is 5.3.10.

Glue Code Instantiation

Cucumber without any DI container instantiates the glue classes, i.e. step definitions and hooks, by creating new instances for each scenario and for each row of a scenario outline. All that is needed is the presence of a default constructor.

When Cucumber is used with dependency injection, the glue classes creation task is handled by the DI container. The instantiation of dependent object network is also managed automatically. Simply put, if a step definition needs a object of class Foo which in turn depends on object of class Bar, the whole chain will be instantiated by the container.

The magic of state sharing happens as the same object instance is injected to all the classes that require it. The dependencies can be injected in the constructor or field. A fresh set of objects are created for each scenario, similar to plain object creation without any container.

The choice of which DI container to use is often dictated by the one already used by the application. If Spring is used in the application it makes sense to use the same in Cucumber. When there is no existing container, PicoContainer is the default choice. Only one of the DI integration jars need to be included.

Spring Container

The Spring container Cucumber integration makes use of field injection. This means that the step definition or hook classes need to declare the dependencies as instance variable. These fields are annotated with the @Autowired Spring annotation.

Implementation

To understand dependency injection with Spring container, a contrived case is created where each step is defined in a separate class. The scenario describes the process of gathering user preferences, such as font and border, for an article or a website.

The feature file can be found at the following location.

Scenario: Collect font, border details
   Given User at preference selection page
   When User selects font details
   | style  | courier |
   | color  | red     |
   | weight | bold    |
   When User selects border details
   | style | dashed |
   | color | orange |
   | width | dark   |
   Then Confirm user selections

The Font and Border objects are modeled in respective POJO’s containing annotations from the Lombok project. These take care of creating getter, setter, constructors and other similar details. Both of them need to be annotated with the Component annotation. The Scope annotation ties the life-cycle of the bean to that of Cucumber glue classes. Below are the important parts of the Font and Border class.

@Data
@Component
@Scope(SCOPE_CUCUMBER_GLUE)
public class Font {
   private String style = "arial";
   private String color = "black";
   private String weight = "normal";
}
@Data
@Component
@Scope(SCOPE_CUCUMBER_GLUE)
public class Border {
   private String style = "dotted";
   private String color = "gray";
   private String width = "default";
}

To enable Spring container DI the cucumber-spring, spring-context-support and spring-test jars need to be included in the POM. The complete POM can be found at the following location.

<dependency>
   <groupId>io.cucumber</groupId>
   <artifactId>cucumber-spring</artifactId>
   <version>6.11.0</version>
   <scope>test</scope>
</dependency>
<dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-context-support</artifactId>
   <version>5.3.10</version>
   <scope>test</scope>
</dependency>
<dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-test</artifactId>
   <version>5.3.10</version>
   <scope>test</scope>
</dependency>

A Spring configuration class needs to be added which mainly defines the packages where the bean classes are located. The glue code packages are scanned by default and added to cucumber-glue scope.

@Configuration
@ComponentScan("preference.data")
public class Config {}

Classes which cannot be edited to include the Component annotation, need to be added to the configuration file with instantiation details.

@Configuration 
@ComponentScan("preference.data") 
public class Config {
   @Bean
   @Scope(SCOPE_CUCUMBER_GLUE)
   public LockedClass instantiateLocked() {
      return new LockedClass();
   }
}

The first step of the scenario is defined in PreferenceStepDefinition which basically just prints out the default values of the Font and Border objects. There are two instance variable of Font and Border types to be injected. These need to be marked by using the Autowired annotation. The ContextConfiguration annotation needs to be added in any one of the step definitions, pointing to the location of the Spring configuration class. If Spring Boot is used, replace the ContextConfiguration with SpringBootTest. The DirtiesContext annotation loads a new application context for each scenario. The CucumberContextConfiguration needs to be added in any step definition class.

@CucumberContextConfiguration
@ContextConfiguration(classes = { Config.class })
@DirtiesContext
public class PreferenceStepDefinition {
   @Autowired   
   private Font font; 
   @Autowired   
   private Border border;

   @Given("User at preference selection page")
   public void user_at_preference_selection_page() {
      System.out.println("Default values are :");      
      System.out.println(font);
      System.out.println(border);
   }
}

The second step of the scenario is defined in FontStepDefinition which updates the shared Font instance with the selected values. There is one instance variable of Font type, annotated with Autowired.

public class FontStepDefinition {
   @Autowired
   private Font font;

   @When("User selects font details")
   public void user_selects_below_font_details(@Transpose Font font) {
      this.font.setStyle(font.getStyle());
      this.font.setColor(font.getColor());
      this.font.setWeight(font.getWeight());
   }
}

The third step of the scenario is defined in BorderStepDefinition which updates the shared Border instance with the selected values. There is one instance variable of Border type, annotated with Autowired.

public class BorderStepDefinition {
   @Autowired
   private Border border;

   @When("User selects border details")
   public void user_selects_below_border_details(@Transpose Border border) {
      this.border.setStyle(border.getStyle());
      this.border.setColor(border.getColor());
      this.border.setWidth(border.getWidth());
   }
}

The final step of the scenario is defined in ResultStepDefinition which prints out the updated values of the Font and Border objects. There are two instance variable of Font and Border types to be injected.

public class ResultStepDefinition {
   @Autowired
   private Font font;
   @Autowired
   private Border border;

   @Then("Confirm user selections")
   public void confirm_user_selections() {
      System.out.println("Results are : ");
      System.out.println(font);
      System.out.println(border);
   }
}

The execution of the scenario using the UserPreferencesTest runner produces the below result.

Default Font & Border options are : 
Font(style=arial, color=black, weight=normal)
Border(style=dotted, color=gray, width=default)

Selected Font options are : 
Font(style=courier, color=red, weight=bold)

Selected Border options are : 
Border(style=dashed, color=orange, width=dark)

Updated Font & Border options are : 
Font(style=courier, color=red, weight=bold)
Border(style=dashed, color=orange, width=dark)

From the result it is clear that the state of the objects is shared across multiple step definitions without the code explicitly instantiating or managing them.

2 thoughts on “Cucumber-JVM Sharing state using Spring Dependency Injection”

Leave a Reply

Your email address will not be published. Required fields are marked *