Sharing state between steps in Cucumber-JVM using Guice

Filed under: Cucumber, Guice, — Tags: Behaviour-Driven Development, Gherkin, Share state between steps, cucumber-guice — Thomas Sundberg — 2017-08-16

A scenario in Gherkin is created by steps. Each step depends on previous steps. This means that we must be able to share state between steps.

The glue between Gherkin and the system under test is implemented as regular Java methods and implemented in regular Java classes. The steps are global in the sense that every step in the same package or subpackage relative to the runner will be found and executed. This allows us to define one step in one class and another step in another class.

Dividing steps between many classes may be a good idea. It is, however, probably not needed early in a project. The first reasonable division should therefore probably be no division. When you write your first scenario, you will most likely only have a few steps. Not more than that they can fit into one class without too much hassle. The first class with steps is probably small and you can easily find your way around in it.

The problem with too large step classes doesn't occur until after a while. You have added a bunch of scenarios to your project and finding your way around in the step definition class is getting harder and harder. The problem with large classes are that they

You would like to split the steps into multiple classes. The question is how.

Splitting according to concept

The good thing with global steps is that they allow us to divide steps along different axes. The decision on how to split is the same as when you decide which functionality goes into which class. This is hard, but something good developers do all the time. The more they learn about the problem and the domain, the more natural the division will be.

One way to split the steps may be according to the domain concept they work on.

Divide steps between different classes according to something that is logical for the team. All steps regarding goods goes into one step class and all steps regarding customers in another. Other axes are possible and perhaps better. If you make a mistake and realize it, move the methods to a better home. Moving the steps around is not a humongous task.

The next problem you will have to solve is how to handle a shared state between the steps. When there was only one class, an instance variable or two was probably enough. Now you need to solve the problem with a shared state between the two, or more, classes with steps.

Dependency injection

How do you share state between different classes?

In Cucumber for Ruby, there is a world object where the shared state lives. It is re-created for each scenario. Each scenario has a fresh world and leakage between scenarios through the world object is unlikely.

A naive solution in Java could be to share a state using a class with static fields. This would work. It is unfortunately very easy for information to leak from one scenario to another. Static fields are not cleared while the JVM is running. To clear them, you would either have to reset them manually or restart the JVM. Both ways are cumbersome.

How do you share state in Java then? The idiomatic solution in Java is to use dependency injection. That is, inject a common object in each class with steps. An object that is recreated every time a new scenario is executed.

Cucumber-JVM support many different dependency injection frameworks. One of them is Guice. Dependencies can be made available where they are needed using annotations.

A small example in two parts

Lets look at a small example and see how it can be implemented with one step definition class and then extend it so the steps are implemented in two different classes with a common object to share state.

The narrative for the example goes like this:

This example can be documented using Gherkin like this this:

src/test/resources/se/thinkcode/refund-faulty-items.feature

Feature: Refund faulty items

  Broken items should be refunded if you have receipt

  Scenario: Returning a broken kettle to the store
    Given that Joanna bought a faulty kettle for $100
    When she return the kettle to the store
    And she show her receipt
    Then she will get $100 refunded

Steps that support this scenario may be implemented as:

src/test/java/se/thinkcode/steps/RefundItems.java

package se.thinkcode.steps;

import cucumber.api.java.en.Given;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;
import se.thinkcode.Customer;
import se.thinkcode.Item;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

public class RefundItems {
    private Customer customer;
    private Item item;

    @Given("^that (.*) bought a faulty (.*) for \\$(\\d+)$")
    public void that_bought_a_faulty_kettle(String name, String itemType, int price) throws Throwable {
        customer = new Customer(name);
        item = new Item(itemType, price);
    }

    @When("^she return the (.*) to the store$")
    public void return_the_an_item_to_the_store(String itemType) throws Throwable {
        Item returnedItem = new Item(itemType);
        assertThat(item, is(returnedItem));
    }

    @When("^she show her receipt$")
    public void she_can_show_a_receipt() throws Throwable {
        customer.refund(item.getPrice());
    }

    @Then("^she will get \\$(\\d+) refunded$")
    public void she_will_get_$_back(int expected) throws Throwable {
        assertThat(customer.getRefunded(), is(expected));
    }
}

A Maven pom file that supports the project may look like this:

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>se.thinkcode.blog</groupId>
    <artifactId>example-no-shared-state</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>info.cukes</groupId>
            <artifactId>cucumber-java</artifactId>
            <version>1.2.5</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>info.cukes</groupId>
            <artifactId>cucumber-junit</artifactId>
            <version>1.2.5</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

It declares two dependencies to Cucumber. This is enough to get the example working.

A runner class that will connect the specification in Gherkin with the steps implemented in Java looks like this:

src/test/java/se/thinkcode/RunCukesTest.java

package se.thinkcode;

import cucumber.api.junit.Cucumber;
import org.junit.runner.RunWith;

@RunWith(Cucumber.class)
public class RunCukesTest {
}

The runner class can be called anything but the Maven test runner searches the class path for classes that starts or ends with the word test. I prefer classes that ends with the word test. This means that naming it RunCukesTest will allow the test runner to find it and execute it as a part of the regular Maven build. It will be executed during the test phase.

This executable specification will be executed when you do

mvn test

Different concepts

This example talks about two different concepts

In a real world implementation, you would probably not split yet because it is so small.

Splitting the steps

One thing to notice is that the feature file and the runner class should not be touched at all when we divide the implementation of the steps into different classes. The only files that are affected are the steps.

My first change is to add new dependencies in the Maven pom.

This dependency prepare Cucumber to use Guice:

<dependency>
    <groupId>info.cukes</groupId>
    <artifactId>cucumber-guice</artifactId>
    <version>1.2.5</version>
    <scope>test</scope>
</dependency>

This is a dependency to Guice itself:

<dependency>
    <groupId>com.google.inject</groupId>
    <artifactId>guice</artifactId>
    <version>4.1.0</version>
    <scope>test</scope>
</dependency>

The new Maven pom now looks like this:

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>se.thinkcode.blog</groupId>
    <artifactId>example-shared-state</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>info.cukes</groupId>
            <artifactId>cucumber-java</artifactId>
            <version>1.2.5</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>info.cukes</groupId>
            <artifactId>cucumber-junit</artifactId>
            <version>1.2.5</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>info.cukes</groupId>
            <artifactId>cucumber-guice</artifactId>
            <version>1.2.5</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.google.inject</groupId>
            <artifactId>guice</artifactId>
            <version>4.1.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

Next step is to create two classes for the steps. I will call them CustomerSteps and GoodsSteps. The idea is that they will share state between steps that depends on the result of an earlier step in the scenario. Sharing state can be done in different ways, I will use a common world object. This solution is very similar to the one of a shared state with a world object in Ruby. Remember that the world object in Ruby, as well as the shared world injected by Guice, are recreated for every scenario. Leakage between scenarios through the world is therefore not possible.

The shared world object looks like this:

src/test/java/se/thinkcode/steps/World.java

package se.thinkcode.steps;

import cucumber.runtime.java.guice.ScenarioScoped;
import se.thinkcode.Customer;
import se.thinkcode.Item;

@ScenarioScoped
public class World {
    Customer customer;
    Item item;
}

It is annotated with @ScenarioScoped so Guice will be able to acknowledge it as something that should be created and made available in different classes. Except for this annotation, this is the smallest change I could make, it only contains references to a Customer and an Item. I decided to make the fields package private so it is easier to refer to them. This is not necessarily the idiomatic way in Java, but I could keep the changes I had to do to my step implementations small.

The implementations of GoodsSteps and CustomerSteps both contain a private field World. I annotated the constructor in GoodsSteps with @Inject. I annotated the field world with @Inject. This is a way to show that you have a choice to annotate a constructor or a field to allow Guice to set the value. This will make the shared world object available to all steps.

src/test/java/se/thinkcode/steps/GoodsSteps.java

package se.thinkcode.steps;

import com.google.inject.Inject;
import cucumber.api.java.en.Given;
import cucumber.runtime.java.guice.ScenarioScoped;
import se.thinkcode.Customer;
import se.thinkcode.Item;

public class GoodsSteps {
    private World world;

    @Inject
    public GoodsSteps(World world) {
        this.world = world;
    }

    @Given("^that (.*) bought a faulty (.*) for \\$(\\d+)$")
    public void that_bought_a_faulty_kettle(String name, String itemType, int price) throws Throwable {
        world.customer = new Customer(name);
        world.item = new Item(itemType, price);
    }
}

The implementation of CustomerSteps looks like this:

src/test/java/se/thinkcode/steps/CustomerSteps.java

package se.thinkcode.steps;

import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;
import cucumber.runtime.java.guice.ScenarioScoped;
import se.thinkcode.Item;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

import com.google.inject.Inject;

public class CustomerSteps {
    @Inject
    private World world;

    @When("^she return the (.*) to the store$")
    public void return_the_an_item_to_the_store(String itemType) throws Throwable {
        Item returnedItem = new Item(itemType);
        assertThat(world.item, is(returnedItem));
    }

    @When("^she show her receipt$")
    public void she_can_show_a_receipt() throws Throwable {
        world.customer.refund(world.item.getPrice());
    }

    @Then("^she will get \\$(\\d+) refunded$")
    public void she_will_get_$_back(int expected) throws Throwable {
        assertThat(world.customer.getRefunded(), is(expected));
    }
}

Execute this with

mvn test

The execution will have the same result as the previous example where the state was shared using instance variables in the step definition class. The difference is that the state shared between the steps now is contained in the world object.

Conclusion

Using Guice to share state between steps in a scenario requires some work. It is a bit intrusive. It may be worth the cost if you already are using Guice in your project for dependency injection.

Acknowledgements

I would like to thank Malin Ekholm for proofreading and helping me to find my typos.

Resources



(less...)

Pages

About
Events
Why

Categories

Agile
Automation
BDD
Clean code
Continuous delivery
Continuous deployment
Continuous integration
Cucumber
Culture
Design
DevOps
Executable specification
Git
Gradle
Guice
J2EE
JUnit
Java
Javascript
Kubernetes
Linux
Load testing
Maven
Mockito
New developers
Pair programming
PicoContainer
Presentation
Programming
Public speaking
Quality
React
Recruiting
Requirements
Scala
Selenium
Software craftsmanship
Software development
Spring
TDD
Teaching
Technical debt
Test automation
Tools
Web
Windows
eXtreme Programming

Authors

Thomas Sundberg
Adrian Bolboaca

Archives

Meta

rss RSS