Skip to content

Task 1 A Cucumber feature

snorrees edited this page Mar 10, 2016 · 2 revisions

Create a Cucumber-feature

For this task, you will be working in the task1 module (<project-root>/task1).


Outline

We will explore the basics of a Cucumber feature specification, and run it using our IDE and Maven.

Steps:

  1. Sniff at the documentation
  2. Setup maven dependencies
  3. Create a JUnit test with Cucumber-testrunner
  4. Create a .feature file
  5. Create a test glue
  6. Parameterize our tests

The Cucumber docs

Most of the topics this workshop will cover, is documented in the Cucumber documentation. Feel free to browse around at any point.

Maven dependencies

To enable Cucumber for our module, add the following dependency:

<dependency>
    <groupId>info.cukes</groupId>
    <artifactId>cucumber-java</artifactId>
    <scope>test</scope>
</dependency>

We will be using the JUnit extensions, which provides a Cucumber-testrunner for JUnit. Add it to the pom.xml:

<dependency>
   <groupId>info.cukes</groupId>
   <artifactId>cucumber-junit</artifactId>
   <scope>test</scope>
</dependency>

In the workshop-repository the version is defined in the <root-dir>/pom.xml under <dependencyManagement> - we do not have to set it in the child-module

Current cucumber-version is 1.2.3.

Java 8 implementation

There exists a Java 8 implementation which uses lambda-expression for hooking up Cucumber. Unfortunately, it exploits the inner workings of the Java implementation for type-transformation, and does not work with the latest Java 8 JDK. To avoid juggling JDK versions during the workshop, we will NOT be using the following dependency:

<dependency>
    <groupId>info.cukes</groupId>
    <artifactId>cucumber-java8</artifactId>
</dependency>

JUnit + Cucumber

So, lets start working with Cucumber.

First, create a new class named Task1Test.

###JUnit runner To use Cucumber with JUnit, we annotate our class with the Cucumber-testrunner.

Annotate your class with:

@RunWith(Cucumber.class)

Verify that Cucumber is hooked up by running the class as a test (using mvn clean test in the task1 directory or directly in your IDE). Output should look something like this:

No features found at [classpath:tasks/task1/start]
0 Scenarios
0 Steps
0m0,000s

Out-of-the box, the Cucumber-testrunner will look for .feature files in our classpath, in the same package as the annotated class.

Since we haven't created any .feature files, nothing much happens.

We should now have a class under <project-root>/task1/src/test/java/task that looks something like this:

@RunWith(Cucumber.class)
public class Task1Test {
}

Feature

Now that we have hooked up Cucumber with JUnit, its time to create our specification.

Cucumber executes .feature files written using "Gherkin". Gherkin is Cucumbers way of structuring natural language so it can be interpreted as executable specification.

Most of the Gherkin-syntax is covered in the reference documentation, and we will work our way through the vocabulary during these tasks.

.feature file

A .feature file describes a single feature of the system, or a particular aspect of a feature. It will typically specify how a business rule is intended to work, and can be regarded as the specification.

Feature

A feature is defined with the keyword Feature: followed a name. Optionally, a multi-line description can be added.

The name and description is just text, the perfect place to explain what the feature is all about, which concepts it covers and the general business rules at play.

Action: Start by creating a file called "hello.feature" in the resource directory of task 1 (task1/src/test/resources/task).

At the top of the file place:

# encoding: utf-8
# language: en

A line starting with # is a comment in Cucumber, but here the comments contains some additional information that tells Cucumber the encoding of the file, as well at the language the feature is written in.

Name the feature, and add a little description:

Feature: The 'Hello World!' of Cucumber
  An example feature, showing of the basics in Cucumber.

Scenario

Next, we add our first Scenario. A Scenario is a concrete example that illustrates a business rule or domain concept; specification by example.

Scenarios are made up of Steps, typically describing an initial context followed by an event, followed by an expected outcome.

Steps within a Scenario typically use the keywords Given, When and Then.

Append the following to the .feature file:

Scenario: All caps transformation
  Given the input text Hello World!
  When the text is transformed into all caps
  Then the transformed text is HELLO WORLD!

Run Task1Test once more. Notice how the output has changed, and now looks something like this:

Test ignored.
1 Scenarios (1 undefined)
3 Steps (3 undefined)
0m0.000s

You can implement missing steps with the snippets below:

@Given("^the input text Hello World!$")
public void the_text_Hello_World() throws Throwable {
    // Write code here that turns the phrase above into concrete actions
    throw new PendingException();
}

@When("^the text is transformed into all caps$")
public void the_text_is_transformed_into_all_caps() throws Throwable {
    // Write code here that turns the phrase above into concrete actions
    throw new PendingException();
}

@Then("^the transformed text is HELLO WORLD!$")
public void the_transformed_text_is_HELLO_WORLD() throws Throwable {
    // Write code here that turns the phrase above into concrete actions
    throw new PendingException();
}

Cucumber has picked up our feature, but it is unable to match it with any Step definitions. However, a clue is given about what we can do to hook up the feature with our Java-code. We need glue code.

Code glue

Create a new class called Task1Glue, copy paste the Java-snippets from the output into the class, and add the following imports:

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

Take note of the en part of the package-names. This is equivalent to the language we embedded in our .feature. Cucumber supports over 60 spoken languages. For instance, the norwegian equivalents of the above imports are:

import cucumber.api.java.en.Gitt;
import cucumber.api.java.en.Når;
import cucumber.api.java.en.;

If you run the test again, you will get yet another result:

1 Scenarios ([33m1 pending)
3 Steps ([36m2 skipped, [33m1 pending)
0m0.108s

cucumber.api.PendingException: TODO: implement me
	at task1_solution.Task1Glue.the_text_Hello_World(Task1Glue.java:16)
	at ✽.Given the text Hello World!(task1_solution/hello.feature:7)

Now, we can implement the missing details. When creating Step definitions, we will typically build our test-state during the @Given steps, trigger some code with the test-state during then @When step and store the result, and @Then use whatever assertion-library we prefer to verify the result.

In this case we want to:

  • Store the string 'Hello World!'
  • Change the string to all capital letters.
  • Assert that the result string is equal to 'HELLO WORLD!'

To achieve this our Task1Glue class will as a bare minimum need:

  • Member field: String text
  • Member field: String result
  • Assign 'Hello World!' to input in the @Given method
  • Transform the input and assign it to result in the @When method
  • Assert that result is equal to 'HELLO WORLD!' in the @Then method

Remove the comments and exceptions from your glue-class, and try to implement the above steps. Feel free to use either JUnit or AssertJ for the assert statements:

  • Assert.assertEquals()
  • Assertions.assertThat().isEqualTo()

The solutions will stick to using AssertJ with static imports.

When you are happy with the implementation, run the tests and check if they passed. Expected output:

1 Scenarios (1 passed)
3 Steps (3 passed)
0m0.129s

Glue so far

If you manage to make the test pass, your Glue class might look something like this:

public class Task1Glue {
    private String text;
    private String result;

    @Given("^the input text Hello World!$")
    public void the_text_Hello_World() {
        text = "Hello World!";
    }

    @When("^the text is transformed into all caps$")
    public void the_text_is_transformed_into_all_caps() {
        result = text.toUpperCase();
    }

    @Then("^the transformed text is HELLO WORLD!$")
    public void the_transformed_text_is_HELLO_WORLD() {
        assertThat(result).isEqualTo("HELLO WORLD!");
    }
}

This is all fine and dandy, but you might be irked by the fact that the input- and result-text is hardcoded. Of course, Cucumber is not that strict! We can parametric our glue-code using regular expressions.

Step parametrization

The annotated value of our Step definitions is parsed as regex by Cucumber. Capturing groups are passed along to our method, where Cucumber expects to find one parameter per captured value.

In our case we can change our @Given and @Then steps to the following:

@Given("^the input text (.+)$")
public void given_the_text(String text) {
    this.text = text;
} 

(...)

@Then("^the transformed text is (.+)$")
public void then_the_transformed_text_is(String expected) {
    assertThat(result).isEqualTo(expected);
}

Make the above modifications, and duplicate the scenario in the feature. Replace "Hello World!" with "some string", and "HELLO WORLD!" with "SOME STRING" in the copied Scenario.

When we run the test, both of the Scenarios should complete successfully.

Scenario Outline

We just defined two virtually identical Scenarios to illustrate two separate inputs and expected outcomes. This can quickly become tedious for examples with several permutations of input.

To combat duplication, Cucumber offers Scenario Outlines. Scenario Outlines are very similar to a Scenarios, but with the addition of adding an Examples: section. The example section uses a simple table-format to create sets of test-parameters.

Rewrite the feature using a Scenario Outline:

  Scenario Outline: Various text capitalized
    Given the input text <Input>
    When the text is transformed into all caps
    Then the transformed text is <Output>
    Examples:
      | Input        | Output       |
      | Hello World! | HELLO WORLD! |
      | some text    | SOME TEXT    |

Here we use to parameterize our Scenario Outline. For every Example row but the first, the Scenario Outiline will be executed once, keeping duplication and clutter to a minimum.

###Cucumber options

Cucumber supports several output formats.

Annotate Task1Test:

@CucumberOptions(plugin = {"pretty", "html:target/cucumber"})

Rerun your tests, and observer the result.

"pretty" indicates that output should pretty-print all the test-steps with their actual values. Notice how the Scenario Outline is repeated for every example-row.

"html:target/cucumber" has activated the creation of a html-report of the test, and stored the results in target/cucumber Navigate there and open the index.html file in your browser.

We will be looking more at @CucumberOptions in the next task.

And then...

Often more than one Step definition of a certain type is necessary. We can repeat a keyword, or we can use the And keyword instead. Lets say we want to assert the length of our text after transformation by expanding our Scenario Outline like so:

  Scenario Outline: Various text capitalized
    Given the input text <Input>
    When the text is transformed into all caps
    Then the transformed text is <Output>
    And the transformed text has length <Length>
    Examples:
      | Input        | Output       |
      | Hello World! | HELLO WORLD! |
      | some text    | SOME TEXT    |

Run this spesification, and implement the missing Step definition in the Task1Glue class.

As we can see, the and step is implemented exactly like a @Then step - the meaning of And is contextual in a Scenario. @When and @Then also supports And.

Building a Step definition library

Our feature is taking shape, but we are repeating ourselves with the phrase 'the transformed text'. Within the context of our Scenario, we might just want to say 'And it has length' to make it easier to read.

One way to fix this is to simply changing 'the transformed text' to 'it' in our @Then definition where we assert length. But what happens then if we change the order of our Then and And steps?

Then it has length <Length>
And the transformed text is <Output>

Does this look right to you?

We can use use non-capturing regex groups to make our Step definitions a little more flexible.

Modify the @Then definitions with more flexible languge:

    @Then("^(?:the transformed text|it) is (.+)$")
    public void then_the_transformed_text_is(String expected) {
        assertThat(result).isEqualTo(expected);
    }

    @Then("^(?:the transformed text|it) has length (\\d+)$")
    public void the_transformed_text_has_length(int expected) {
        assertThat(result.length()).isEqualTo(expected);
    }

Try running the tests again, but make some adjustments to the Outline:

Then the transformed texthas length <Length>
And it is <Output>

or

Then the transformed text is <Output>
And it has length <Length>

As you can see, we can add quite a bit of flexibility in formulating our Scenarios, making code-reuse easier and more flexible. Over time, we can build a library of step definitions, which can express complex domain problems.

Summary

Thats it! You completed the first task.

So far we've seen how to:

  • Create a JUnit test powered by Cucumber
  • Create a Feature using Gherkin
  • Create Step definitions in a glue class
  • Parameterize Step definitions using regular expressions
  • Use Scenario Outlines for similar examples

In the next task we will look at more advanced features, using tidbits of code from a small game.