Why separate test definitions from a driving JUnit class?

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.

Coupling step definitions to a particular feature

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.

Runner agnostic

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.

Single Responsibility Principle - SRP

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.

The correct way

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

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

Maven

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>

Acknowledgements

This post has been reviewed by some people who I wish to thank for their help

Thank you very much for your feedback!

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