Filed under: Cucumber, Spring, — Tags: Behaviour-Driven Development, Gherkin, Share state between steps, cucumber-spring — Thomas Sundberg — 2017-06-24
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 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 Spring. 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 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 kettle to the store$") public void return_the_an_item_to_the_store() 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 Spring:
<dependency> <groupId>info.cukes</groupId> <artifactId>cucumber-spring</artifactId> <version>1.2.5</version> <scope>test</scope> </dependency>
These two dependencies are Spring itself:
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>4.3.8.RELEASE</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>4.3.8.RELEASE</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-spring</artifactId> <version>1.2.5</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>4.3.8.RELEASE</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>4.3.8.RELEASE</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 Spring, 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 org.springframework.stereotype.Component; import se.thinkcode.Customer; import se.thinkcode.Item; @Component public class World { Customer customer; Item item; }
It is annotated with @Component
so Spring 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 contains a private field World
that are annotated with @Autowired
. This means that Spring will set the value of the field and the shared world
will be available to all steps.
src/test/java/se/thinkcode/steps/GoodsSteps.java
package se.thinkcode.steps; import cucumber.api.java.en.Given; import org.springframework.beans.factory.annotation.Autowired; import se.thinkcode.Customer; import se.thinkcode.Item; public class GoodsSteps { @Autowired private 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
is annotated in the same way as GoodsSteps
and world
is available to all steps.
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 org.springframework.beans.factory.annotation.Autowired; import se.thinkcode.Item; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; public class CustomerSteps { @Autowired private World world; @When("^she return the kettle to the store$") public void return_the_an_item_to_the_store() 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)); } }
The last part needed is to configure cucumber.xml
. The only project specific configuration is the package Spring should examine to find components to inject. I set it to se.thinkcode
that reflects that the root package for this project is se.thinkcode
. The complete file looks like this:
src/test/resources/cucumber.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd"> <context:component-scan base-package="se.thinkcode"/> </beans>
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.
Using Spring to share state between steps in a scenario requires some work. It is a bit intrusive. But it doesn't require more work than described here.
I would like to thank Malin Ekholm and Johan Helmfrid for proofreading and helping me to find my typos.