Cucumber-JVM Sharing state using PicoContainer 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 PicoContainer to manage state.

The source code can be found at this location. The cucumber version used is 6.11.0. The PicoContainer version is 2.15.

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 classes 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.

PicoContainer

It is the easiest and most unobtrusive DI container to use. The PicoContainer Cucumber integration makes use of constructor injection. This means that a constructor specifying the necessary dependencies is required for a step definition or hook class. It is advisable to create one constructor so object creation is easy to predict. When multiple constructors are present, PicoContainer uses the one with the greatest number of arguments.

Implementation

To understand dependency injection with PicoContainer, 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. Below are the important parts of the Font and Border class.

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

To enable PicoContainer DI the cucumber-picocontainer jar needs to be included in the POM. The complete POM can be found at the following location. The picocontainer jar is a transitive dependency of the cucumber-picocontainer jar.

<dependency>
   <groupId>io.cucumber</groupId>
   <artifactId>cucumber-picocontainer</artifactId>
   <version>6.11.0</version>
   <scope>test</scope>
</dependency>

The first step of the scenario is defined in PreferenceStepDefinition which basically just prints out the default values of the Font and Border objects. The constructor of this step definition class has two arguments, Font and Border types, which are required for injection.

public class PreferenceStepDefinition {
   private Font font; 
   private Border border;

   public PreferenceStepDefinition(Font font, Border border) {
      this.font = font;
      this.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. The constructor of this step definition class has one argument of Font type.

public class FontStepDefinition {
   private Font font;

   public FontStepDefinition(Font font) {
      this.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. The constructor of this step definition class has one argument of Border type.

public class BorderStepDefinition {
   private Border border;

   public BorderStepDefinition(Border border) {
      this.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. The constructor of this step definition class has two arguments, Font and Border types.

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

   public ResultStepDefinition(Font font, Border border) {
      this.font = font;
      this.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, Font and Border, is shared across multiple step definitions without the code explicitly instantiating or managing them.

Constructor Strategies

As already mentioned, the dependencies to be injected in the step definition or hook class needs to be added to the constructor. In case of web browser automation it would primarily be various page objects.

In Cucumber projects the instance fields of the dependency classes are primarily string or primitive types, mostly containing data from scenarios. It is easier to handle object creation with the default constructor. So the best option would be to have no constructors as PicoContainer will choose the one with the largest number of arguments.

To get a deeper understanding of how PicoContainer deals with constructors refer to this.

In the case of a dependency class which requires constructor arguments, thereby a default constructor is not possible, one option is to wrap this inside another class with a default constructor. The wrapper class instance is then shared. The instantiation of the wrapped class is then handled manually inside a step definition.

The Foo class only has a two argument constructor, generated by the AllArgsConstructor annotation, which initializes the instance fields. The FooWrapper wraps the Foo class and enables object state sharing.

@Data
@AllArgsConstructor
public class Foo {
   private String dataOne;
   private String dataTwo;
}
@Data
public class FooWrapper {
   private Foo foo;
}

The FooStepDefinition and BarStepDefinition demonstrates how the wrapper enables sharing of the Foo object.

public class FooStepDefinition {
   private FooWrapper fooWrap;
   private Foo foo;

   public FooStepDefinition(FooWrapper fooWrapper) {
      this.fooWrap = fooWrapper;
   }

   @Given("Initial value is")
   public void initial_value_is() {
      System.out.println(foo);
   }

   @When("Foo values are {string} and {string}")
   public void foo_values_are_and(String one, String two) {
      foo = new Foo(one,two);
      this.fooWrap.setFoo(foo);
   }
}
public class BarStepDefinition {
   private Foo foo;

   public BarStepDefinition(FooWrapper fooWrapper) {
      this.foo = fooWrapper.getFoo();
   }
	
   @Then("Final value is")
   public void final_value_is() {
      System.out.println(foo);
   }
}

Below is the result of the execution of the scenario using the WrapperTest runner.

Initial value is : null

Final Value : Foo(dataOne=First Value, dataTwo=Second Value)

9 thoughts on “Cucumber-JVM Sharing state using PicoContainer Dependency Injection”

  1. Getting following warning message in feature file and unable to find the steps but the steps got executed while run.
    Step ‘I enter “” and submit page’ does not have a matching glue code for example on line 10

    I am using Pico container , is there anything need to do to fix the

    1. I am unable to find the feature file u r referring to. Can u mention the link here? Or is this your own project? Thx.

      1. yes this my own project only, the problem what i am facing is , the matching step definition is working fine while we o the execution but the feature file showing the warning message.

        1. For other project(which not using the Pico container ) not showing the warning message and I can able to access the step diff from the feature.

          My Concern is
          is there anything need to when we use the Pico container ?

            1. I just checked on my eclipse and it gives the same warning. To be honest, I never much cared about it as the tests run perfectly.

              I think u can get a resolution for this on stackoverflow. Thanks

Leave a Reply

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