Filed under: Cucumber, PicoContainer, — Tags: Behaviour-Driven Development, Gherkin, Share state between steps, cucumber-picocontainer — Thomas Sundberg — 2017-04-01
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 are 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 in 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 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.
Dependency injection can be done in many ways. A simple solution is to to inject dependencies through the constructor. This is sometimes referred to as Constructor Dependency Injection, CDI.
Cucumber-JVM support many different dependency injection frameworks. One of the least intrusive frameworks is called PicoContainer. It is a minimalistic framework that is invisible everywhere except in the build script where a dependency to it has to be declared.
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 this 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
Because it is so small, you would probably not split it yet in a real world implementation.
One thing to notice is that the feature file and the runner class should not be touched at all. They are unaffected by the separation of steps.
My first change is to add a dependency to PicoContainer, a dependency injection framework, in the Maven pom.
<dependency> <groupId>info.cukes</groupId> <artifactId>cucumber-picocontainer</artifactId> <version>1.2.5</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-picocontainer</artifactId> <version>1.2.5</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 PicContainer, 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 se.thinkcode.Customer; import se.thinkcode.Item; public class World { Customer customer; Item item; }
It is as small as I could make it. It only contains a reference to a Customer and a reference to an Item. I decided to make the fields package private to make it easy to refer to them. This is not necessarily the idiomatic way in Java, but it made the changes I had to do to my step implementations small.
The implementations of GoodsSteps
and CustomerSteps
both contains a constructor that requires an instance of the world object. Since I included PicoContainer, the classes will be created and injected using this constructor with the same instance of world
.
src/test/java/se/thinkcode/steps/GoodsSteps.java
package se.thinkcode.steps; import cucumber.api.java.en.Given; import se.thinkcode.Customer; import se.thinkcode.Item; public class GoodsSteps { private World world; 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
has an identical constructor as GoodsSteps
.
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 se.thinkcode.Item; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; public class CustomerSteps { private World world; public CustomerSteps(World world) { this.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)); } }
Executing this 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.
I would have preferred not to create a world
and share state through it. It would have been nicer to do the sharing using a Customer
and an Item
object. This would have been possible if the domain objects I used hadn't required parameters in their constructors.
Using PicoContainer to share state between steps in a scenario is easy and non intrusive. All you need is a constructor that requires an object that PicoContainer can create and inject.
PicoContainer is invisible. Add a dependency to cucumber-picocontainer
and make sure that the constructors for the step classes requires an instance of a the same class.
I would like to thank Malin Ekholm for proofreading and helping me to find my typos.