A Cucumber Experiment

Having used the GildedRose recently as the subject of a coding dojo, I thought it would also make an interesting subject for some experimentation with Cucumber. Cucumber is a tool that allows you to use natural language to specify executable Acceptance Tests. The GildedRose exercise, originally created by Bobby Johnson, can be found on github, in both C# (the original), and Java (my copy). This code kata is a refactoring assignment, where the requirements are given together with a piece of existing code and the programmer is expected to add some functionality. If you take one look at that existing code, though, you’ll see that you really need to clean it up before adding that extra feature:

    public static void updateQuality()
    {
        for (int i = 0; i < items.size(); i++)
        {
            if ((!"Aged Brie".equals(items.get(i).getName())) && !"Backstage passes to a TAFKAL80ETC concert".equals(items.get(i).getName()))
            {
                if (items.get(i).getQuality() > 0)
                {
                    if (!"Sulfuras, Hand of Ragnaros".equals(items.get(i).getName()))
                    {
                        items.get(i).setQuality(items.get(i).getQuality() - 1);
                    }
                }
            }
            else
            {
                if (items.get(i).getQuality() < 50)
                {
                    items.get(i).setQuality(items.get(i).getQuality() + 1);

                    if ("Backstage passes to a TAFKAL80ETC concert".equals(items.get(i).getName()))
                    {
                        if (items.get(i).getSellIn() < 11)
                        {
                            if (items.get(i).getQuality() < 50)
                            {
                                items.get(i).setQuality(items.get(i).getQuality() + 1);
                            }
                        }

                        if (items.get(i).getSellIn() < 6)
                        {
                            if (items.get(i).getQuality() < 50)
                            {
                                items.get(i).setQuality(items.get(i).getQuality() + 1);
                            }
                        }
                    }
                }
            }

            if (!"Sulfuras, Hand of Ragnaros".equals(items.get(i).getName()))
            {
                items.get(i).setSellIn(items.get(i).getSellIn() - 1);
            }

            if (items.get(i).getSellIn() < 0)
            {
                if (!"Aged Brie".equals(items.get(i).getName()))
                {
                    if (!"Backstage passes to a TAFKAL80ETC concert".equals(items.get(i).getName()))
                    {
                        if (items.get(i).getQuality() > 0)
                        {
                            if (!"Sulfuras, Hand of Ragnaros".equals(items.get(i).getName()))
                            {
                                items.get(i).setQuality(items.get(i).getQuality() - 1);
                            }
                        }
                    }
                    else
                    {
                        items.get(i).setQuality(items.get(i).getQuality() - items.get(i).getQuality());
                    }
                }
                else
                {
                    if (items.get(i).getQuality() < 50)
                    {
                        items.get(i).setQuality(items.get(i).getQuality() + 1);
                    }
                }
            }
        }
    }

Having gone through the exercise a few times, I already had a version lying around that had the requirements implemented as a bunch of junit regression tests, and some real unit tests for my implementation, of course. A good starting point to get going with Cuke, though in many real-life situations I’ve found that those regression tests are not available…

Adding Cucumber to the maven build

To prepare for the use of Cucumber, I first had to set it up so that I had all the dependencies for cucumber, and have it run in the maven integration-test phase. The complete Maven pom.xml is also on GitHub.

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>GildedRoseJava</groupId>
    <artifactId>GildedRoseJava</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.8.2</version>
            <type>jar</type>
            <scope>test</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.picocontainer</groupId>
            <artifactId>picocontainer</artifactId>
            <version>2.10.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>cuke4duke</groupId>
            <artifactId>cuke4duke</artifactId>
            <version>0.4.4</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <repositories>
        <repository>
            <id>codehaus</id>
            <url>http://repository.codehaus.org</url>
        </repository>
        <repository>
            <id>cukes</id>
            <url>http://cukes.info/maven</url>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>cukes</id>
            <url>http://cukes.info/maven</url>
        </pluginRepository>
    </pluginRepositories>
    <build>
        <plugins>
            <plugin>
                <groupId>cuke4duke</groupId>
                <artifactId>cuke4duke-maven-plugin</artifactId>
                <configuration>
                    <jvmArgs>
                        <jvmArg>
                            -Dcuke4duke.objectFactory=cuke4duke.internal.jvmclass.PicoFactory
                        </jvmArg>
                        <jvmArg>-Dfile.encoding=UTF-8</jvmArg>
                    </jvmArgs>
                    <!-- You may not need all of these arguments in your
          own project. We have a lot here for testing purposes... -->
                    <cucumberArgs>
                        <cucumberArg>--backtrace</cucumberArg>
                        <cucumberArg>--color</cucumberArg>
                        <cucumberArg>--verbose</cucumberArg>
                        <cucumberArg>--format</cucumberArg>
                        <cucumberArg>pretty</cucumberArg>
                        <cucumberArg>--format</cucumberArg>
                        <cucumberArg>junit</cucumberArg>
                        <cucumberArg>--out</cucumberArg>
                        <cucumberArg>${project.build.directory}/cucumber-reports</cucumberArg>
                        <cucumberArg>--require</cucumberArg>
                        <cucumberArg>${basedir}/target/test-classes</cucumberArg>
                    </cucumberArgs>
                    <gems>
                        <gem>install cuke4duke --version 0.3.2</gem>
                    </gems>
                </configuration>
                <executions>
                    <execution>
                        <id>run-features</id>
                        <phase>integration-test</phase>
                        <goals>
                            <goal>cucumber</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

Adding a Feature

To start with cucumber, you create a feature file that contains a Feature, which is expected to be in the User Story schema, and contains all the Acceptance Tests for that story in the Gherkin format.

The feature file is placed in the {$project.root}/features directory. The Story looks as follows:

Feature: Quality changes with sell-in date Feature
         In order to keep track of quality of items in stock
         As a ShopKeeper
         I want quality to change as the sell-in date decreases

A Scenario

This doesn’t do anything, until you start adding some scenarios, which can be seen as the acceptance criteria for the story/feature. For now, if you do

mvn integration-test

you’ll get confirmation that Cucumber has found your feature, and that there were no test scenarios to run:

[INFO] Code:
[INFO]
[INFO] Features:
[INFO]   * features/quality_decrease.feature
[INFO] Parsing feature files took 0m0.111s
[INFO]
[INFO] Code:
[INFO]   * /home/wouter/data/eclipse/kata/GildedRoseGitHub/target/test-classes/org/geenz/gildedrose/BackstagePassStoreKeepingItemTest.class
[INFO]   * /home/wouter/data/eclipse/kata/GildedRoseGitHub/target/test-classes/org/geenz/gildedrose/GildedRoseRegressionTest.class
[INFO]   * /home/wouter/data/eclipse/kata/GildedRoseGitHub/target/test-classes/org/geenz/gildedrose/GildedRoseTest.class
[INFO]   * /home/wouter/data/eclipse/kata/GildedRoseGitHub/target/test-classes/org/geenz/gildedrose/ImmutableItemTest.class
[INFO]   * /home/wouter/data/eclipse/kata/GildedRoseGitHub/target/test-classes/org/geenz/gildedrose/ItemFactoryTest.class
[INFO]   * /home/wouter/data/eclipse/kata/GildedRoseGitHub/target/test-classes/org/geenz/gildedrose/StoreKeepingItemTest.class
[INFO]   * /home/wouter/data/eclipse/kata/GildedRoseGitHub/target/test-classes/org/geenz/gildedrose/cucumber/BasicFeature.class
[INFO]
[INFO] Feature: Quality changes with sell-in date Feature
[INFO]   In order to keep track of quality of items in stock
[INFO]   As a ShopKeeper
[INFO]   I want quality to change as the sell-in date decreases
[INFO]
[INFO] 0 scenarios
[INFO] 0 steps
[INFO] 0m0.062s

Now, let’s add a scenario, let’s say a scenario for the basic situation of quality decreasing by one if the sell-in date decreases by one:

Feature: Quality changes with sell-in date Feature
        In order to keep track of quality of items in stock
        As a ShopKeeper
        I want quality to change as the sell-in date decreases

        Scenario: Decreasing Quality of a Basic Item
                Given a Store Keeping Item with name "+5 Dexterity Vest"
                And a sellIn date of 5
                And a quality of 7
                When the Item is updated
                Then the sellIn is 4
                And the quality is 6

There’s a few things to notice here. First of all, this is readable. This scenario can be read by people that have no programming experience, and understood well if they have some knowledge of the domain of the application. That means that scenarios like this one can be used to talk about the requirements/expected behaviour of the application. Second is that this is a Real Life example of the workings of the application. Making it a specific example, instead of a generic rule, makes the scenario easier to understand, and thus more useful as a communication device.

Glue

So what happens when we try to run the integration-test phase again? Will we magically see this scenario work? Well, no, there’s still some glue to provide, but we do get some help with that:

[INFO] Code:
[INFO]
[INFO] Features:
[INFO]   * features/quality_decrease.feature
[INFO] Parsing feature files took 0m0.077s
[INFO]
[INFO] Code:
[INFO]   * /home/wouter/data/eclipse/kata/GildedRoseGitHub/target/test-classes/org/geenz/gildedrose/BackstagePassStoreKeepingItemTest.class
[INFO]   * /home/wouter/data/eclipse/kata/GildedRoseGitHub/target/test-classes/org/geenz/gildedrose/GildedRoseRegressionTest.class
[INFO]   * /home/wouter/data/eclipse/kata/GildedRoseGitHub/target/test-classes/org/geenz/gildedrose/GildedRoseTest.class
[INFO]   * /home/wouter/data/eclipse/kata/GildedRoseGitHub/target/test-classes/org/geenz/gildedrose/ImmutableItemTest.class
[INFO]   * /home/wouter/data/eclipse/kata/GildedRoseGitHub/target/test-classes/org/geenz/gildedrose/ItemFactoryTest.class
[INFO]   * /home/wouter/data/eclipse/kata/GildedRoseGitHub/target/test-classes/org/geenz/gildedrose/StoreKeepingItemTest.class
[INFO]   * /home/wouter/data/eclipse/kata/GildedRoseGitHub/target/test-classes/org/geenz/gildedrose/cucumber/BasicFeature.class
[INFO]
[INFO] Feature: Quality changes with sell-in date Feature
[INFO]   In order to keep track of quality of items in stock
[INFO]   As a ShopKeeper
[INFO]   I want quality to change as the sell-in date decreases
[INFO]
[INFO]   Scenario: Decreasing Quality of a Basic Item               # features/quality_decrease.feature:6
[INFO]     Given a Store Keeping Item with name "+5 Dexterity Vest" # features/quality_decrease.feature:7
[INFO]     And a sellIn date of 5                                   # features/quality_decrease.feature:8
[INFO]     And a quality of 7                                       # features/quality_decrease.feature:9
[INFO]     When the Item is updated                                 # features/quality_decrease.feature:10
[INFO]     Then the sellIn is 4                                     # features/quality_decrease.feature:11
[INFO]     And the quality is 6                                     # features/quality_decrease.feature:12
[INFO]
[INFO] 1 scenario (1 undefined)
[INFO] 6 steps (6 undefined)
[INFO] 0m0.221s
[INFO]
[INFO] You can implement step definitions for undefined steps with these snippets:
[INFO]
[INFO] @Given ("^a Store Keeping Item with name \"([^\"]*)\"$")
[INFO] @Pending
[INFO] public void aStoreKeepingItemWithName+5DexterityVest_(String arg1) {
[INFO] }
[INFO]
[INFO] @Given ("^a sellIn date of 5$")
[INFO] @Pending
[INFO] public void aSellInDateOf5() {
[INFO] }
[INFO]
[INFO] @Given ("^a quality of 7$")
[INFO] @Pending
[INFO] public void aQualityOf7() {
[INFO] }
[INFO]
[INFO] @When ("^the Item is updated$")
[INFO] @Pending
[INFO] public void theItemIsUpdated() {
[INFO] }
[INFO]
[INFO] @Then ("^the sellIn is 4$")
[INFO] @Pending
[INFO] public void theSellInIs4() {
[INFO] }
[INFO]
[INFO] @Then ("^the quality is 6$")
[INFO] @Pending
[INFO] public void theQualityIs6() {
[INFO] }
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 9.500s
[INFO] Finished at: Fri Sep 16 14:27:29 CEST 2011
[INFO] Final Memory: 5M/106M
[INFO] ------------------------------------------------------------------------

Ok! The build is successful, the new scenario is found, but apparently ‘undefined’, and there’s a load of javacode dumped! Things are moving along…

We copy-past-clean-up the java code into a test class, such as this:

package org.geenz.gildedrose.cucumber;

import cuke4duke.annotation.I18n.EN.Given;
import cuke4duke.annotation.I18n.EN.Then;
import cuke4duke.annotation.I18n.EN.When;
import cuke4duke.annotation.Pending;
import org.geenz.gildedrose.ItemFactory;
import org.geenz.gildedrose.StoreKeepingItem;

import static org.junit.Assert.*;

public class BasicFeature {

	 @Given ("^a Store Keeping Item with name \"([^\"]*)\"$")
	 @Pending
	 public void aStoreKeepingItemWithName+5DexterityVest_(String arg1) {
	 }

	 @Given ("^a sellIn date of 5$")
	 @Pending
	 public void aSellInDateOf5() {
	 }

	 @Given ("^a quality of 7$")
	 @Pending
	 public void aQualityOf7() {
	 }

	 @When ("^the Item is updated$")
	 @Pending
	 public void theItemIsUpdated() {
	 }

	 @Then ("^the sellIn is 4$")
	 @Pending
	 public void theSellInIs4() {
	 }

	 @Then ("^the quality is 6$")
	 @Pending
	 public void theQualityIs6() {
	 }
}

Pending

And, after noticing and correcting that a ‘+’ sign can’t be part of a Java method name, we run it again, Sam.

Oh dear:

[INFO] Feature: Quality changes with sell-in date Feature
[INFO] In order to keep track of quality of items in stock
[INFO] As a ShopKeeper
[INFO] I want quality to change as the sell-in date decreases
[INFO]
[INFO] Scenario: Decreasing Quality of a Basic Item # features/quality_decrease.feature:6
[INFO] Given a Store Keeping Item with name "+5 Dexterity Vest" # BasicFeature.aStoreKeepingItemWithNamePlus5DexterityVest_(String)
[INFO] TODO (Cucumber::Pending)
[INFO] /home/wouter/.m2/repository/.jruby/gems/cucumber-0.8.7/lib/cucumber/step_match.rb:26:in `invoke'
[INFO] /home/wouter/.m2/repository/.jruby/gems/cucumber-0.8.7/lib/cucumber/ast/step_invocation.rb:62:in `invoke'
[INFO] /home/wouter/.m2/repository/.jruby/gems/cucumber-0.8.7/lib/cucumber/ast/step_invocation.rb:41:in `accept'
[INFO] /home/wouter/.m2/repository/.jruby/gems/cucumber-0.8.7/lib/cucumber/ast/tree_walker.rb:99:in `visit_step'
[INFO] /home/wouter/.m2/repository/.jruby/gems/cucumber-0.8.7/lib/cucumber/ast/tree_walker.rb:164:in `broadcast'
[INFO] /home/wouter/.m2/repository/.jruby/gems/cucumber-0.8.7/lib/cucumber/ast/tree_walker.rb:98:in `visit_step'
[INFO] /home/wouter/.m2/repository/.jruby/gems/cucumber-0.8.7/lib/cucumber/ast/step_collection.rb:15:in `accept'
[IN
.....

Just what a Java developer likes: Ruby stackstraces! Luckily, the first entry is fairly clear: TODO (Cucumber::Pending).
And indeed, now that we take another look at it, the methods we just created all have a @Pending annotation. If we remove that, the scenario ‘runs’ successfully:

[INFO] Feature: Quality changes with sell-in date Feature
[INFO]   In order to keep track of quality of items in stock
[INFO]   As a ShopKeeper
[INFO]   I want quality to change as the sell-in date decreases
[INFO]
[INFO]   Scenario: Decreasing Quality of a Basic Item               # features/quality_decrease.feature:6
[INFO]     Given a Store Keeping Item with name "+5 Dexterity Vest" # BasicFeature.aStoreKeepingItemWithNamePlus5DexterityVest_(String)
[INFO]     And a sellIn date of 5                                   # BasicFeature.aSellInDateOf5()
[INFO]     And a quality of 7                                       # BasicFeature.aQualityOf7()
[INFO]     When the Item is updated                                 # BasicFeature.theItemIsUpdated()
[INFO]     Then the sellIn is 4                                     # BasicFeature.theSellInIs4()
[INFO]     And the quality is 6                                     # BasicFeature.theQualityIs6()
[INFO]
[INFO] 1 scenario (1 passed)
[INFO] 6 steps (6 passed)
[INFO] 0m0.269s

Adding code

Of course, it doesn’t actually test anything yet! But we can take a look at the way the text of the scenario is translated into executable code. For instance:

	 @Given ("^a sellIn date of 5$")
	 public void aSellInDateOf5() {
	 }

This looks straightforward. The @Given annotation gets a regular expression passed to it, which matches a particular condition. If the method actually set a sell-in value on some object, this would be a perfectly valid step in performing some test.

So let’s see where we can create such a sellable item. This other method looks promising:

	 @Given ("^a Store Keeping Item with name \"([^\"]*)\"$")
	 public void aStoreKeepingItemWithNamePlus5DexterityVest_(String arg1) {
	 }

For the non-initiated into the esoteric realm of regular expressions, the regexp here looks a little more scary. It really isn’t all that bad, though. The backslashes (\) are there to escape out the quotes (“). The brackets (()) are there to capture a specific region to be passed into the method: anything within those brackets is passed in as the first parameter of the method (out arg1 String parameter). And to specify what we want to capture, the [^\”]* simply means any character that is not a quote. So this captures everything within the quotes in our scenario, which happens to be the name of the item.

So let’s change that into:

    private StoreKeepingItem item = null;

    @Given ("^a Store Keeping Item with name \"([^\"]*)\"$")
    public void aStoreKeepingItemWithName(String name) {
        item = ItemFactory.create(name, 0, 0);
    }

Now our first line will create a new Item, with the correct name! That was easy!
Now we can fix the earlier method:

	 @Given ("^a sellIn date of 5$")
	 public void aSellInDateOf5() {
             item.setSellIn(5);
	 }

But, since I now already know how to parameterise these annotations, let’s doe that immediately:

    @Given("^a sellIn date of ([0-9]*)$")
    public void aSellInDateOf(int sellIn) {
        item.setSellIn(sellIn);
    }

Much better. Now let’s imagine we’ve done this for the rest as well (click to see):

package org.geenz.gildedrose.cucumber;

import cuke4duke.annotation.I18n.EN.Given;
import cuke4duke.annotation.I18n.EN.Then;
import cuke4duke.annotation.I18n.EN.When;
import cuke4duke.annotation.Pending;
import org.geenz.gildedrose.ItemFactory;
import org.geenz.gildedrose.StoreKeepingItem;

import static org.junit.Assert.*;

public class BasicFeature {

    private StoreKeepingItem item = null;

    @Given ("^a Store Keeping Item with name \"([^\"]*)\"$")
    public void aStoreKeepingItemWithName(String name) {
        item = ItemFactory.create(name, 0, 0);
    }

    @Given("^a sellIn date of ([0-9]*)$")
    public void aSellInDateOf(int sellIn) {
        item.setSellIn(sellIn);
    }

    @Given("^a quality of ([0-9]*)$")
    public void aQualityOf(int quality) {
        item.setQuality(quality);
    }

    @When("^the Item is updated$")
    public void theItemIsUpdated() {
        item.doDailyInventoryUpdate();
    }

    @Then("^the sellIn is (.*)$")
    public void theSellInIs(int expectedSellIn) {
        assertEquals(expectedSellIn, item.getSellIn());
    }

    @Then("^the quality is ([0-9]*)$")
    public void theQualityIs(int expectedQuality) {
        assertEquals(expectedQuality, item.getQuality());
    }
}

The test still passes, so this seems to work! For the rest of the scenarios, see the checked-in feature file on GitHub. Take a look at that file, and compare it to the README. Which one is clearer to you? Do they both contain all the information you need?

The odd one out

Note that there is one scenario there that is not covered by the methods that we have, so once you try to run this particular scenario, things fall apart:

        Scenario: Quality and SellIn of a Sulfuras item does not change
                Given a Store Keeping Item with name "Sulfuras, Hand of Ragnaros" with a sellIn date of 5, a quality of 7
                When the Item is updated
                Then the sellIn is 5
                And the quality is 7

Since this item is supposed to be immutable, I can’t really go and set the sellIn or quality after it’s been created. So I made a separate method to pass-in the sell-in and quality at initialisation time:

    /**
     * Separate when clause to indicate initial state, since some items can't be changed after initial creation.
     */
    @Given ("^a Store Keeping Item with name \"([^\"]*)\" with a sellIn date of ([0-9]*), a quality of ([0-9]*)$")
    public void aStoreKeepingItemWithNameAndSellInDateAndQualityOf(String name, int sellIn, int quality) {
        item = ItemFactory.create(name, sellIn, quality);

There could be a nicer way to do this, either by creating all items in this way, or by changing the way the immutable items work, but this was easy enough to do that I didn’t look any further.

So when we run all the scenarios, we get:

[INFO] Feature: Quality changes with sell-in date Feature
[INFO]   In order to keep track of quality of items in stock
[INFO]   As a ShopKeeper
[INFO]   I want quality to change as the sell-in date decreases
[INFO]
[INFO]   Scenario: Decreasing Quality of a Basic Item               # features/basic.feature:6
[INFO]     Given a Store Keeping Item with name "+5 Dexterity Vest" # BasicFeature.aStoreKeepingItemWithName(String)
[INFO]     And a sellIn date of 5                                   # BasicFeature.aSellInDateOf(int)
[INFO]     And a quality of 7                                       # BasicFeature.aQualityOf(int)
[INFO]     When the Item is updated                                 # BasicFeature.theItemIsUpdated()
[INFO]     Then the sellIn is 4                                     # BasicFeature.theSellInIs(int)
[INFO]     And the quality is 6                                     # BasicFeature.theQualityIs(int)
[INFO]
[INFO]   Scenario: Quality decrease doubles after sell-in has passed # features/basic.feature:14
[INFO]     Given a Store Keeping Item with name "+5 Dexterity Vest"  # BasicFeature.aStoreKeepingItemWithName(String)
[INFO]     And a sellIn date of 0                                    # BasicFeature.aSellInDateOf(int)
[INFO]     And a quality of 10                                       # BasicFeature.aQualityOf(int)
[INFO]     When the Item is updated                                  # BasicFeature.theItemIsUpdated()
[INFO]     Then the sellIn is -1                                     # BasicFeature.theSellInIs(int)
[INFO]     And the quality is 8                                      # BasicFeature.theQualityIs(int)
[INFO]
[INFO]   Scenario: Quality never becomes negative                   # features/basic.feature:22
[INFO]     Given a Store Keeping Item with name "+5 Dexterity Vest" # BasicFeature.aStoreKeepingItemWithName(String)
[INFO]     And a sellIn date of 0                                   # BasicFeature.aSellInDateOf(int)
[INFO]     And a quality of 0                                       # BasicFeature.aQualityOf(int)
[INFO]     When the Item is updated                                 # BasicFeature.theItemIsUpdated()
[INFO]     Then the sellIn is -1                                    # BasicFeature.theSellInIs(int)
[INFO]     And the quality is 0                                     # BasicFeature.theQualityIs(int)
[INFO]
[INFO]   Scenario: Quality of Aged Brie increases with age  # features/basic.feature:30
[INFO]     Given a Store Keeping Item with name "Aged Brie" # BasicFeature.aStoreKeepingItemWithName(String)
[INFO]     And a sellIn date of 5                           # BasicFeature.aSellInDateOf(int)
[INFO]     And a quality of 1                               # BasicFeature.aQualityOf(int)
[INFO]     When the Item is updated                         # BasicFeature.theItemIsUpdated()
[INFO]     Then the sellIn is 4                             # BasicFeature.theSellInIs(int)
[INFO]     And the quality is 2                             # BasicFeature.theQualityIs(int)
[INFO]
[INFO]   Scenario: Quality of Aged Brie never increases past 50 # features/basic.feature:38
[INFO]     Given a Store Keeping Item with name "Aged Brie"     # BasicFeature.aStoreKeepingItemWithName(String)
[INFO]     And a sellIn date of 5                               # BasicFeature.aSellInDateOf(int)
[INFO]     And a quality of 50                                  # BasicFeature.aQualityOf(int)
[INFO]     When the Item is updated                             # BasicFeature.theItemIsUpdated()
[INFO]     Then the sellIn is 4                                 # BasicFeature.theSellInIs(int)
[INFO]     And the quality is 50                                # BasicFeature.theQualityIs(int)
[INFO]
[INFO]   Scenario: Quality of Backstage Passes increases by 1 if sell-in is greater than 10 # features/basic.feature:46
[INFO]     Given a Store Keeping Item with name "Backstage passes to a TAFKAL80ETC concert" # BasicFeature.aStoreKeepingItemWithName(String)
[INFO]     And a sellIn date of 11                                                          # BasicFeature.aSellInDateOf(int)
[INFO]     And a quality of 20                                                              # BasicFeature.aQualityOf(int)
[INFO]     When the Item is updated                                                         # BasicFeature.theItemIsUpdated()
[INFO]     Then the quality is 21                                                           # BasicFeature.theQualityIs(int)
[INFO]
[INFO]   Scenario: Quality of Backstage Passes increases by 2 if sell-in is less than 10 but more than 5 # features/basic.feature:53
[INFO]     Given a Store Keeping Item with name "Backstage passes to a TAFKAL80ETC concert"              # BasicFeature.aStoreKeepingItemWithName(String)
[INFO]     And a sellIn date of 6                                                                        # BasicFeature.aSellInDateOf(int)
[INFO]     And a quality of 20                                                                           # BasicFeature.aQualityOf(int)
[INFO]     When the Item is updated                                                                      # BasicFeature.theItemIsUpdated()
[INFO]     Then the quality is 22                                                                        # BasicFeature.theQualityIs(int)
[INFO]
[INFO]   Scenario: Quality of Backstage Passes increases by 3 if sell-in is 5 or less but more than 0 # features/basic.feature:60
[INFO]     Given a Store Keeping Item with name "Backstage passes to a TAFKAL80ETC concert"           # BasicFeature.aStoreKeepingItemWithName(String)
[INFO]     And a sellIn date of 5                                                                     # BasicFeature.aSellInDateOf(int)
[INFO]     And a quality of 20                                                                        # BasicFeature.aQualityOf(int)
[INFO]     When the Item is updated                                                                   # BasicFeature.theItemIsUpdated()
[INFO]     Then the quality is 23                                                                     # BasicFeature.theQualityIs(int)
[INFO]
[INFO]   Scenario: Quality of Backstage Passes is 0 after the concert (sell-in) passes      # features/basic.feature:67
[INFO]     Given a Store Keeping Item with name "Backstage passes to a TAFKAL80ETC concert" # BasicFeature.aStoreKeepingItemWithName(String)
[INFO]     And a sellIn date of 0                                                           # BasicFeature.aSellInDateOf(int)
[INFO]     And a quality of 20                                                              # BasicFeature.aQualityOf(int)
[INFO]     When the Item is updated                                                         # BasicFeature.theItemIsUpdated()
[INFO]     Then the quality is 0                                                            # BasicFeature.theQualityIs(int)
[INFO]
[INFO]   Scenario: Quality and SellIn of a Sulfuras item does not change                                             # features/basic.feature:74
[INFO]     Given a Store Keeping Item with name "Sulfuras, Hand of Ragnaros" with a sellIn date of 5, a quality of 7 # BasicFeature.aStoreKeepingItemWithNameAndSellInDateAndQualityOf(String,int,int)
[INFO]     When the Item is updated                                                                                  # BasicFeature.theItemIsUpdated()
[INFO]     Then the sellIn is 5                                                                                      # BasicFeature.theSellInIs(int)
[INFO]     And the quality is 7                                                                                      # BasicFeature.theQualityIs(int)
[INFO]
[INFO]   Scenario: Quality of Conjured items goes down twice as fast as a normal item before sell-in # features/basic.feature:80
[INFO]     Given a Store Keeping Item with name "Conjured Mana Cake"                                 # BasicFeature.aStoreKeepingItemWithName(String)
[INFO]     And a sellIn date of 5                                                                    # BasicFeature.aSellInDateOf(int)
[INFO]     And a quality of 20                                                                       # BasicFeature.aQualityOf(int)
[INFO]     When the Item is updated                                                                  # BasicFeature.theItemIsUpdated()
[INFO]     Then the quality is 18                                                                    # BasicFeature.theQualityIs(int)
[INFO]
[INFO]   Scenario: Quality of Conjured items goes down twice as fast as a normal item after sell-in # features/basic.feature:87
[INFO]     Given a Store Keeping Item with name "Conjured Mana Cake"                                # BasicFeature.aStoreKeepingItemWithName(String)
[INFO]     And a sellIn date of 0                                                                   # BasicFeature.aSellInDateOf(int)
[INFO]     And a quality of 20                                                                      # BasicFeature.aQualityOf(int)
[INFO]     When the Item is updated                                                                 # BasicFeature.theItemIsUpdated()
[INFO]     Then the quality is 16                                                                   # BasicFeature.theQualityIs(int)
[INFO]
[INFO] 12 scenarios (12 passed)
[INFO] 64 steps (64 passed)
[INFO] 0m1.617s
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 10.861s
[INFO] Finished at: Fri Sep 16 15:57:23 CEST 2011
[INFO] Final Memory: 5M/106M
[INFO] ------------------------------------------------------------------------

So now we have a full set of acceptance tests, covering the whole of the requirements, and with only very limited amount of code needed. Of course, the small amount of code needed is for a large part because I already refactored the original to something more managable. I would be a nice next experiment to start with these scenarios, and grow the code from there. If you do this, let me know, and send a pull request!

Parameterization

For some type of tests, it makes sense to have a scenario where you put in different types of data, and expect different results. By separating the scenario from the input and output data, you can make thes kind of tests much more readable. In the case of our example, you can find the same tests in the parameterised.feature file, but it’s small enough to simply include here:

Feature: Quality changes with sell-in date Feature
        In order to keep track of quality of items in stock
        As a ShopKeeper
        I want quality to change as the sell-in date decreases

        Scenario Outline: Changing Quality of an Item
                Given a Store Keeping Item with name "<item name>"
                And a sellIn date of <sell in>
                And a quality of <quality>
                When the Item is updated
                Then the sellIn is <expected sell in>
                And the quality is <expected quality>

		Examples:
		| item name			| sell in	| quality	| expected sell in	| expected quality	|
		| +5 Dexterity Vest	| 5			| 7			| 4					| 6					|
		| +5 Dexterity Vest	| 0			| 10		| -1				| 8					|
		| +5 Dexterity Vest	| 0			| 0			| -1				| 0					|
		| Aged Brie			| 5			| 1			| 4					| 2					|
		| Aged Brie			| 5			| 50		| 4					| 50				|
		| Backstage passes to a TAFKAL80ETC concert	| 11 | 20 | 10		| 21				|
		| Backstage passes to a TAFKAL80ETC concert	| 6  | 20 | 5		| 22				|
		| Backstage passes to a TAFKAL80ETC concert	| 5  | 20 | 4		| 23				|
		| Backstage passes to a TAFKAL80ETC concert	| 0  | 20 | -1		| 0					|
		| Conjured Mana Cake| 5			| 20		| 4					| 18				|
		| Conjured Mana Cake| 0			| 20		| -1				| 16				|

As you can see, this is much shorter. It does skip on the detailed text for each scenario, though, which I thought to be somewhat of a loss in this particular case. For tests with larger sets of data, this is probably a great feature, though. For this example I though the feature was clearer with the more verbose scenarios.

If you want to know more, a great resource is the cuke4ninja free on-line book. The Cucumber wiki, and additional tutorials list are also great sources of knowledge.

3 thoughts on “A Cucumber Experiment