Duplicate step definitions in Gherkin & Cucumber (using IntelliJ)

Continuing from my last post Gherkin & Cucumber in Java (using IntelliJ).

Let’s assume you have a project set-up as per my previous post, delete any existing feature files and let’s create two very simple and very similar feature files

Feature: Calculator Add

  Scenario: Add two numbers
    Given Two input values, 1 and 2
    When I add the two values
    Then I expect the result 3
Feature: Calculator Subtract

  Scenario: Subtract two numbers
    Given Two input values, 1 and 2
    When I subtract the two values
    Then I expect the result -1

When we start creating our step definition files we might start with the Add feature and create something like AddStepdefs.java which includes the following code

package com.putridparrot;

import cucumber.api.PendingException;
import cucumber.api.java.en.Given;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;

public class AddStepdefs {
    @Given("^Two input values, (-?\\d+) and (-?\\d+)$")
    public void twoInputValuesAnd(int a, int b) throws Throwable {
    }

    @When("^I add the two values$")
    public void iAddTheTwoValues() throws Throwable {
    }

    @Then("^I expect the result (-?\\d+)$")
    public void iExpectTheResult(int r) throws Throwable {
    }
}

Next we might start creating the step definitions file for the Subtract feature and notice, IntelliJ shows one step as already existing (i.e. it’s not got a highlight background colour). If you press the CTRL key and move the mouse over the step which appear to exist already (i.e. Two input values, 1 and 2) a hyperlink line will appear, click this and it will take you to the previously added step in the AddStepdefs file.

What’s happening here is that whilst we might define separate classes per feature (which may well seem a logical way to write our test code/step definitions), Cucumber is actually matching to methods based upon the RegEx within the annotations, i.e. Given Two input values, 1 and 2 maps to

@Given("^Two input values, (-?\\d+) and (-?\\d+)$")
public void twoInputValuesAnd(int a, int b) throws Throwable {
}

Cucumber doesn’t actually take notice of the Given/When/Then annotations for matching the method to the line of Gherkin code.

Let us assume that we simply copy the missing step into the SubtractStepdefs.java file, we now have duplicate step definitions according to Cucumber, which is ofcourse correct if we think that each step is in essence globally scoped by Cucumber.

Or to put it another way, Cucumber will search through all the packages within the “Glue” package(s) to locate matching RegEx’s. If it finds more than one matching RegEx we get a duplicate step error. Here’s the (truncated) error that Cucumber will display for us when we try to run all features.

Exception in thread "main" cucumber.runtime.DuplicateStepDefinitionException: Duplicate step definitions in com.putridparrot.SubtractStepdefs.twoInputValuesAnd(int,int)

Handling duplicate step definitions

So how do we resolve a situation where we want to run all features and we have duplicate steps?

The easiest solution is, ensure you never have duplicate steps unless you intended to reuse the same step definition code – the general idea is to think of the language used to define a step in Gherkin as a specific task and another step using the same text is really the same task (at least when associated with the same package).

Ofcourse we can have duplicate steps in different packages, we just need to ensure we run features against on that package using the Glue option, i.e. do not reference all packages but just point to the one’s specific to the features.

If we therefore have the code like this in AddStepdefs and not in SubtractStepdefs

public class AddStepdefs {
   private int value1;
   private int value2;

   @Given("^Two input values, (-?\\d+) and (-?\\d+)$")
   public void twoInputValuesAnd(int a, int b) throws Throwable {
      value1 = a;
      value2 = b;  
   }
   // other code removed
}

the we have created another problem. The instance variables, value1 and value2 or stored in AddStepdefs and hence we’d also have similar variables stored in SubtractStepdefs (for use with the subtract scenario) however, the twoInputValuesAnd will never change the instance variables in the SubtractStepdefs for obvious reasons…

So how do we share instance data across our step definition files?

As programmers we might see a Scenario as analogous to a class but you can see that to ensure we adhere to the DRY principle we’d actually be better off creating a single class with all step definitions across all scenarios. This ofcourse is also not ideal because once we start to rack up a large number of scenarios, our class will become large and unwieldy.

So what we really want to do is create an instance of some shared state and have Cucumber pass this to each step definition class.

This is where cucumber-picocontainer comes in. If we add the following to the pom.xml

<dependency>
   <groupId>info.cukes</groupId>
   <artifactId>cucumber-picocontainer</artifactId>
   <version>1.2.5</version>
   <scope>test</scope>
</dependency>

and can “context” like this

public class ScenarioContext {
    private int value1;
    private int value2;
    private int result;

    public int getValue1() {
        return value1;
    }

    public void setValue1(int value) {
        this.value1 = value;
    }

    public int getValue2() {
        return value2;
    }

    public void setValue2(int value) {
        this.value2 = value;
    }

    public int getResult() {
        return result;
    }

    public void setResult(int value) {
        this.result = value;
    }
}

We can now change our step definitions files to look like this, first the AddStepdefs file

public class AddStepdefs {

    private ScenarioContext context;

    public AddStepdefs(ScenarioContext context) {
        this.context = context;
    }

    @Given("^Two input values, (-?\\d+) and (-?\\d+)$")
    public void twoInputValuesAnd(int a, int b) throws Throwable {
        context.setValue1(a);
        context.setValue2(b);
    }

    @When("^I add the two values$")
    public void iAddTheTwoValues() throws Throwable {
        Calculator calculator = new Calculator();

        context.setResult(calculator.add(context.getValue1(), context.getValue2()));
    }

    @Then("^I expect the result (-?\\d+)$")
    public void iExpectTheResult(int r) throws Throwable {
        Assert.assertEquals(r, context.getResult());
    }
}

and the SubtractStepdefs without any duplicated steps could look like this

public class SubtractStepdefs {

    private ScenarioContext context;

    public SubtractStepdefs(ScenarioContext context) {
        this.context = context;
    }

    @When("^I subtract the two values$")
    public void iSubtracTheTwoValues() throws Throwable {
        Calculator calculator = new Calculator();

        context.setResult(calculator.subtract(context.getValue1(), context.getValue2()));
    }
}

When a feature file is run, Cucumber will get an instance of our ScenarioContext passed to each class by the picocontainer which will them be used by the class methods.