The Secret Ninja Cucumber Scrolls

Sharing context across step definition files

Putting all our ninjas in a single file is a good trick, made popular by the Black Star Ninja in the famous documentary about the life of Ninjas on Philippines from the 80's, wrongly titled The American Ninja. However, sometimes it is useful to break our step definitions into several files, and then share the context between them. Cucumber supports this with all the platforms, but in different ways.

So far, all our steps have been in a single file, corresponding to a feature file. As things get more complex, you'll want to write feature files that reuse some of the steps from other feature files and combine them with other steps, so the mapping between a step definition and a feature file is not necessarily the same as between Subzero and Scorpion. In this section, we'll show aspiring Ninja Cucumberists how to split these apart.

To demonstrate that, let's add a new feature file that reuses some of the steps that we already defined and adds some new steps. Paste the following content in features/split.feature:

Feature: Split Brains
  Ninjas can receive training to withstand a direct hit on the head

  Scenario: Samurai katana is useless against a ninja >= 3rd level
    Given the ninja has a third level black-belt
    When hit on the head by a samurai with a katana
    Then the ninja's brains should not be harmed

  Scenario: Ninja training is useless against Chuck Norris
    Given the ninja has a third level black-belt
    When hit on the head by Chuck Norris with a fist
    Then the ninja's brains should split  

The initial step Given the ninja has a third level black-belt will be already implemented. This step previously created a Ninja object that we'll share with another step definition file.

Sharing context in Ruby

Ruby loads step definitions from any files in the step_definitions directory. They do not have to all be in a single file. To see that, run the following command line:

cucumber features/split.feature

Cucumber will execute just the new feature and report that it found the first step in ninja_steps.rb but that the other steps are still undefined (see Figure 6.3, “Cucumber finds some split ninja steps”).

Figure 6.3. Cucumber finds some split ninja steps

Cucumber finds some split ninja steps

Now we can add another step definition that completes the impact analysis required to decide whether the brains of a ninja should not be harmed or be split after a direct hit. Just to demonstrate that things can be a bit more complex, we'll put this logic into an impact calculator. The step definition file just needs to read the ninja created by the step in ninja_steps.rb using the same class variable name @ninja. When Cucumber compiles the steps, both references to @ninja will map to the same value.

require 'rspec/expectations' 
require 'cucumber/formatter/unicode'

$:.unshift(File.dirname(__FILE__) + '/../../src')
require 'ninja'
require 'impact_calculator'

When /^hit on the head by [a ]*([A-z ]*) with a ([A-z]*)$/ do |opponent, weapon|
  @opponent=opponent
  @weapon=weapon
end

Then /^the ninja's ([A-z]*) should ([A-z]*)$/ do |target,expected_impact|
  impact_c=ImpactCalculator.new
  actual_impact=impact_c.impact @ninja,@opponent,@weapon,@target
  actual_impact.should==expected_impact 
end

Then /^the ninja's ([A-z]*) should not be harmed$/ do |target|
  impact_c=ImpactCalculator.new
  actual_impact=impact_c.impact @ninja,@opponent,@weapon,@target
  actual_impact.should=="not harmed"
end

We'll leave the ImpactCalculator class to you for homework, or if you want to cheat go and get it from the Cuke4Ninja github repository[25] - look for a file called impact_calculator.rb.

Sharing context in Java

Cuke4Duke loads step definitions from any classes annotated with Cucumber annotations such as @Given, they to not have to be all in the same file. To see that, run your tests again after adding a new feature file. For example, if running with maven, run the following command line:

mvn integration-test

Cucumber will execute the new feature file and find some of the steps in the old NinjaSkillSteps.java step definition class, but report other steps as still undefined (see Figure 6.4, “Cuke4Duke finds some split ninja steps”).

Figure 6.4. Cuke4Duke finds some split ninja steps

Cuke4Duke finds some split ninja steps

The first step, from NinjaSkillSteps.java, creates an internal Ninja object. Because we want to be able to reuse that object in two different step definition classes, we have to enable another step definition class to somehow see that state. Before you even think about saying ‘static’ know that Chuck Norris moves static objects. Remember that you had to add PicoContainer or Spring to the classpath? Now you'll finally understand why. Cuke4Duke shares state between step definitions using dependency injection. We can just create a class to hold the context required for our steps and add an instance of that class as a constructor argument for step definition classes. Cuke4Duke will ensure that both classes get the same context. Let's do that.

First, let's define a context class. We just need to share the Ninja object so far, so let's create a container for a ninja:

package ninjasurvivalrate;

public class NinjaContext{
  private Ninja ninja;
  public void setNinja(Ninja ninja){
    this.ninja=ninja;
  }
  public Ninja getNinja(){
    return this.ninja;
  }
}

Now let's change the existing NinjaSurvivalSteps.java to expect an instance of this new class in the constructor:

public class NinjaSurvivalSteps {
 private NinjaContext ninjaContext; 
 public NinjaSurvivalSteps(NinjaContext ninjaContext){
   this.ninjaContext=ninjaContext;
 }

At this point, you should be able to run the tests again and not notice any difference in the old functionality. Although the old step definition class did not have a constructor and this one does, and it requires a context parameter, Cuke4Duke wires that in automatically using your selected dependency injection container

Now we can add another step definition class with a similar constructor to handle the missing steps:

package ninjasurvivalrate;

import cuke4duke.annotation.I18n.EN.Given;
import cuke4duke.annotation.I18n.EN.Then;
import cuke4duke.annotation.I18n.EN.When;
import java.util.Map;
import java.util.List;
import java.util.Collection;

import static junit.framework.Assert.assertEquals;

public class SplitSteps {
 private NinjaContext ninjaContext;
 private String opponent;
 private String weapon; 
 public SplitSteps(NinjaContext ninjaContext){
   this.ninjaContext=ninjaContext;
 }
 @When ("^hit on the head by [a ]*([A-z ]*) with a ([A-z]*)$")
 public void hitOnTheHead(String opponent, String weapon) {
  this.opponent=opponent;
  this.weapon=weapon;
 }
 @Then ("^the ninja's ([A-z]*) should not be harmed$")
 public void shouldNotBeHarmed(String target) {
   expectImpact(target,"not harmed");
 }
 @Then ("^the ninja's ([A-z]*) should ([A-z]*)$")
 public void expectImpact(String target, String expectedImpact) {
  String actualImpact=ninjaContext.getNinja().impact(target,opponent,weapon);
  assertEquals(expectedImpact,actualImpact); 
 }
}

We'll leave it to you for homework to implement the missing function for impact analysis in the Ninja class and to change the rest of the NinjaSurvivalSteps.java to use the context Ninja instead of the private one. If you want to cheat or admit that you are lazy, look for a folder called NinjaSurvivalRateWithSharedState in the Cuke4Ninja code repository[26].

Sharing context in .NET

Cuke4Nuke loads step definitions from any classes annotated with Cucumber annotations such as [Given], they to not have to be all in the same file. To see that, run your tests again after adding a new feature file.

Cucumber will execute the new feature file and find some of the steps in the old NinjaSteps.cs step definition class, but report other steps as still undefined.

The first step, from NinjaSteps.cs, creates an internal Ninja object. Because we want to be able to reuse that object in two different step definition classes, we have to enable another step definition class to somehow see that state. Before you even think about saying ‘static’ know that Chuck Norris moves static objects. Cuke4Nuke shares state between step definitions using dependency injection. We can just create a class to hold the context required for our steps and add an instance of that class as a constructor argument for step definition classes. Cuke4Nuke will ensure that both classes get the same context. Let's do that.

First, let's define a context class. We just need to share the Ninja object so far, so let's create a container for a ninja:

Ôªønamespace NinjaSurvivalRate
{
    public class NinjaContext
    {
        public Ninja Ninja{get; set;}
    }
}

Now let's change the existing NinjaSteps.cs to expect an instance of this new class in the constructor:

        public NinjaSteps(NinjaContext ninjaContext)
        {
            _ninjaContext = ninjaContext;
        }

At this point, you should be able to run the tests again and not notice any difference in the old functionality. Although the old step definition class did not have a constructor and this one does, and it requires a context parameter, Cuke4Nuke wires that in automatically using your selected dependency injection container

Now we can add another step definition class with a similar constructor to handle the missing steps:

Ôªøusing Cuke4Nuke.Framework;
using NUnit.Framework;

namespace NinjaSurvivalRate
{
    public class SplitSteps
    {
        private string _opponent;
        private string _weapon;
        private readonly NinjaContext _ninjaContext;

        public SplitSteps(NinjaContext ninjaContext)
        {
            _ninjaContext = ninjaContext;
        }

        [When(@"^hit on the head by [a ]*([A-z ]*) with a ([A-z]*)$")]
        public void HitOnHead(string opponent, string weapon)
        {
            _opponent = opponent;
            _weapon = weapon;
        }

        [Then(@"^the ninja's ([A-z]*) should ([A-z]*)$")]
        public void ExpectImpact(string target, string expectedmpact)
        {
            string actualImpact = _ninjaContext.Ninja.CalculateImpact(_opponent);
            Assert.AreEqual(expectedmpact, actualImpact);            
        }

        [Then(@"^the ninja's ([A-z]*) should not be harmed$")]
        public void NinjaNotHarmed(string target)
        {
            ExpectImpact(target, "not harmed");
        }
    }
}

We'll leave it to you for homework to implement the missing function for impact analysis in the Ninja class and to change the rest of the NinjaSteps.cs to use the context Ninja, not the private one. If you want to cheat or admit that you are lazy, look for a folder called NinjaSurvivalRateWithSharedState in the Cuke4Ninja code repository[27].