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.
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.
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.
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
This example talks about two different concepts
In a real world implementation, you would probably not split yet because it is so small.
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.
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.
I would like to thank Malin Ekholm for proofreading and helping me to find my typos.