Filed under: Cucumber, — Tags: Cucumber-jvm, JUnit, Java — Thomas Sundberg — 2011-10-31
Why should the step definitions be separated from the driving JUnit class when using Cucumber for Java?
An example of a setup using Cucumber for Java could look like this:
package se.sigma.cucumber; import cucumber.annotation.en.Given; import cucumber.annotation.en.Then; import cucumber.annotation.en.When; import cucumber.junit.Cucumber; import cucumber.junit.Feature; import org.junit.runner.RunWith; @RunWith(Cucumber.class) public class DataRoamingStepAndTest { @Given("^I am on a ship to Finland$") public void I_am_on_a_ship_to_Finland() { // Express the Regexp above with the code you wish you had } @Then("^I verify that I have data roaming off on my telephone$") public void I_verify_that_I_have_data_roaming_off_on_my_telephone() { // Express the Regexp above with the code you wish you had } @When("^it departs from the harbor$") public void it_departs_from_the_harbor() { // Express the Regexp above with the code you wish you had } }
This class mixes JUnit and the step definitions into one class. This may seem like a good idea. We need to drive the feature using some mechanism and it is nice to use JUnit because it is supported in many environments.
There are, however, a few problems when mixing steps and the driving class.
A feature describes a behaviour. If you use the same description to describe two different behaviours of a system, then you have an ambiguous behaviour. This is therefore a smell that indicates that the system isn't clearly defined.
Steps in Cucumber are defined global. One particular step definition can be used in more then one feature, but it may not be executed in more then one place. The rational behind this global definition is to make sure that the system isn't using the same description to describe different parts.
Cucumber is not tied to JUnit. The steps can be executed using any runner. Not allowing to tie the step definitions to a JUnit class with specific runner is a consequence of this desired behaviour.
The single responsibility principle says that everything should have one and only one responsibility. The class above has two responsibilities. It connects the feature to the step definitions, which is an undesired behaviour, and implements the steps. This responsibility should be split to two or more classes.
A another approach is to separate the step definitions and the JUnit class that can drive them into two separate classes. The result looks like this:
File: src/test/java/se/sigma/cucumber/DataRoamingTest.java package se.sigma.cucumber; import cucumber.junit.Cucumber; import org.junit.runner.RunWith; @RunWith(Cucumber.class) public class DataRoamingTest { }
An empty JUnit class that runs the steps defined in the feature.
File: src/test/java/se/sigma/cucumber/DataRoamingSteps.java package se.sigma.cucumber; import cucumber.annotation.en.Given; import cucumber.annotation.en.Then; import cucumber.annotation.en.When; public class DataRoamingSteps { @Given("^I am on a ship to Finland$") public void I_am_on_a_ship_to_Finland() { // Express the Regexp above with the code you wish you had } @Then("^I verify that I have data roaming off on my telephone$") public void I_verify_that_I_have_data_roaming_off_on_my_telephone() { // Express the Regexp above with the code you wish you had } @When("^it departs from the harbor$") public void it_departs_from_the_harbor() { // Express the Regexp above with the code you wish you had } }
The steps are implemented in a separate class that may be used by any feature.
This is the preferred way and it is actually being forced since late October 2010. A CucumberException will be thrown if the test class contains any methods.
The feature used in this example is not very interesting. But it look like this:
File: src/test/resources/se/sigma/cucumber/DataRoaming.feature Feature: Avoid high data roaming charges As an owner of a mobile telephone I want to avoid high roaming charges So I can use my money to buy beer instead Scenario: Travelling to Finland Given I am on a ship to Finland When it departs from the harbor Then I verify that I have data roaming off on my telephone
The Maven pom used is defined as:
File: pom.xml <?xml version="1.0" encoding="UTF-8"?> <project> <modelVersion>4.0.0</modelVersion> <groupId>se.sigma.cucumber</groupId> <artifactId>separate-steps-and-junit</artifactId> <version>1.0-SNAPSHOT</version> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.10</version> <scope>test</scope> </dependency> <dependency> <groupId>info.cukes</groupId> <artifactId>cucumber-java</artifactId> <version>1.0.1</version> <scope>test</scope> </dependency> <dependency> <groupId>info.cukes</groupId> <artifactId>cucumber-junit</artifactId> <version>1.0.1</version> <scope>test</scope> </dependency> </dependencies> </project>
This post has been reviewed by some people who I wish to thank for their help
Thank you very much for your feedback!