BDD with Cucumber-JVM at GeeCON TDD 2015

Filed under: BDD, Cucumber, Java, — Tags: Behaviour driven development, Cucumber-jvm, GeeCON — Thomas Sundberg — 2015-01-30

This blog post is the same as the example I presented at GeeCON TDD in Poznan, Poland, January 2015. It is a step-by-step example that I hope you will be able to follow and implement yourself.

But before I begin with the implementation, let me reason about why you should care about BDD.

Behaviour Driven Development, BDD, is a way to try to bridge the gap between developers, who can read code, and people who are less fluent in reading code. Cucumber is not a tool only for acceptance testing. It is a communication tool where you can express examples in plain text that anyone can read. These examples can also be executed. They are the outcome from discussions between stakeholders, developers and testers.

Given this, the technical part of BDD that I will show you is the less important part. The most important part is the conversations that occurs and defines the application that should be implemented.

Without further ado, lets start with an implementation of a small project using BDD and Cucumber-JVM.

Setting up the infrastructure

The simplest way to get started with this project is to define a Maven project and let Maven handle all the fuzz with the dependency handling. A Maven pom that will be enough for this project is the one below.

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>se.thinkcode</groupId>
    <artifactId>geecon-tdd-2015</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>info.cukes</groupId>
            <artifactId>cucumber-java</artifactId>
            <version>1.2.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>info.cukes</groupId>
            <artifactId>cucumber-junit</artifactId>
            <version>1.2.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

This pom defines three dependencies that we need. It defines two for Cucumber and a unit testing framework, JUnit. The rest defines project names and version. Nothing important for this discussion.

Cucumber can be executed in a few different ways. There is a command line tool that can be used and there is a JUnit runner. If I use the JUnit runner, it becomes very easy to run Cucumber from Maven during the test phase. This, in turn is very nice because it enables us to run Cucumber as an integrated part of our Continuous Integration, CI, build.

The benefits of integrating the execution in the test phase are so large that I will do that. This means that I need to implement a JUnit class and annotate it with the name of a runner that will be executed when the tests are executed by Maven.

An implementation of the test class may look like this:

src/test/java/se/thinkcode/geecon/RunCukesTest.java

package se.thinkcode.geecon;

import cucumber.api.junit.Cucumber;
import org.junit.runner.RunWith;

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

There are two things to note here.

The annotation means that the JUnit runner will be an implementation that is aware of Cucumber and searches for features to execute.

The class doesn't have any methods and the reason for this is that the steps that will be executed as part of a feature must be externalized to another class. The reason for this is an urge to separate concerns. The JUnit class run Cucumber and nothing else. The steps that should be executed must be implemented somewhere else. They are not the responsibility of the runner. Cucumber will throw an exception if you implement any method in the test class.

Describe the feature in Gherkin for Cucumber

This is all the infrastructure needed. Let us now continue with the interesting parts. I will start by implementing a feature. A feature is written using Gherkin. Gherkin is a small language with a only a few keywords. It should be defined in a file with the file ending feature and must be available in the class path. Cucumber will search the class path for any files called .feature. Maven will make sure everything in the resources directory available on the class path. This means that any feature defined in resources will be picked up. It turns out that it is almost that simple, but just almost. Cucumber also requires that the feature file is in the same package as the runner or any sub package. Given this, let me add a feature file like the one below.

src/test/resources/se/thinkcode/geecon/belly.feature

# language: pl
Funkcja: Ogórkowa-JVM

  W celu zaprezentowania pakietu Ogórkowa-JVM
  Chciałbym przedstawić praktyczny przykład tak aby wszyscy mogli zobaczyć w jaki sposób można go zastosować

  Scenariusz: Burczenie w brzuchu
    Mając 42 ogórki w brzuchu
    Kiedy odczekam 1 godzinę
    Wtedy mój brzuch zacznie burczeć

Oops, this is in polish. If you are like me, then this is hard to understand. I don't read or speak Polish well enough to understand this. But it is valid Gherkin and it can be used by Cucumber. An English translation may look like this:

Feature: Cucumber-JVM should be introduced

  In order to present Cucumber-JVM
  As a speaker
  I want to develop a working example where the audience can see how it is possible to execute an example

  Scenario: Belly growl
    Given I have 42 cukes in my belly
    When I wait 1 hour
    Then my belly should growl

This is also Gherkin and easier for me to understand. These two features say the same thing. Given that I have eaten a lot of cukes, my belly should growl after a while.

The trick to get Cucumber to understand Polish is the first line in the feature file. The line # language: pl will convince the Gherkin parser to use its polish translation. This means that the annotations used later can be either annotations translated into Polish or annotations in English, that is @Given.

Nothing should happen if I run the test class above. Let me try it and see what happens. I will use Maven and execute the command mvn clean install. The result will probably be that you download half of the Internet if this your first execution of Maven. If you have used Maven before, then you might have some dependencies cached locally and only need to download a small fraction of the net.

Fail the test

The execution log for Maven may be overwhelming but the interesting part is the test execution. It should look something similar to this excerpt:

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running se.thinkcode.geecon.RunCukesTest

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


You can implement missing steps with the snippets below:

@Mając("^(\\d+) ogórki w brzuchu$")
public void ogórki_w_brzuchu(int arg1) throws Throwable {
    // Write code here that turns the phrase above into concrete actions
    throw new PendingException();
}

@Kiedy("^odczekam (\\d+) godzinę$")
public void odczekam_godzinę(int arg1) throws Throwable {
    // Write code here that turns the phrase above into concrete actions
    throw new PendingException();
}

@Wtedy("^mój brzuch zacznie burczeć$")
public void mój_brzuch_zacznie_burczeć() throws Throwable {
    // Write code here that turns the phrase above into concrete actions
    throw new PendingException();
}

Tests run: 5, Failures: 0, Errors: 0, Skipped: 4, Time elapsed: 0.68 sec

Results :

Tests run: 5, Failures: 0, Errors: 0, Skipped: 4

Cucumber is telling me that there are three steps defined in Gherkin, but it can't find any implementation that matches the steps. Cucumber is, however, nice and suggests code stubs that can be used as a basis for an implementation.

Make it work

I will copy the suggested steps and paste them into a new Java class. I will call it StepDefinitions and place it in the same package as the runner, se.thinkcode.geecon

The first implementation will look like this:

src/test/java/se/thinkcode/geecon/StepDefinitions.java

package se.thinkcode.geecon;

import cucumber.api.PendingException;
import cucumber.api.java.pl.Kiedy;
import cucumber.api.java.pl.Mając;
import cucumber.api.java.pl.Wtedy;

public class StepDefinitions {
    @Mając("^(\\d+) ogórki w brzuchu$")
    public void ogórki_w_brzuchu(int arg1) throws Throwable {
        // Write code here that turns the phrase above into concrete actions
        throw new PendingException();
    }

    @Kiedy("^odczekam (\\d+) godzinę$")
    public void odczekam_godzinę(int arg1) throws Throwable {
        // Write code here that turns the phrase above into concrete actions
        throw new PendingException();
    }

    @Wtedy("^mój brzuch zacznie burczeć$")
    public void mój_brzuch_zacznie_burczeć() throws Throwable {
        // Write code here that turns the phrase above into concrete actions
        throw new PendingException();
    }
}

The most interesting parts here are the annotations above each method. These annotations are used to connect the step defined in Gherkin with the method in Java. The regular expressions in the annotations are used to match a method with a step.

The groups in the regular expression, the stuff between the parenthesis, are used to match parameters to the method. This is the way Cucumber get actual values to operate on from the examples.

Running Maven again results in something like this:

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running se.thinkcode.geecon.RunCukesTest

1 Scenarios (1 pending)
3 Steps (2 skipped, 1 pending)
0m0.230s

cucumber.api.PendingException: TODO: implement me
	at se.thinkcode.geecon.StepDefinitions.ogórki_w_brzuchu(StepDefinitions.java:12)
	at *.Mając 42 ogórki w brzuchu(se/thinkcode/geecon/belly.feature:8)

Tests run: 5, Failures: 0, Errors: 0, Skipped: 4, Time elapsed: 0.94 sec

This tells us that there are steps and matching methods defined. It also tells us that the steps implemented are throwing a pending exception and wants to be properly implemented.

My next task is to implement the feature. I will drive the implementation from my steps. This is the way it usually is done, the implementation is driven from the outside in. You will most likely switch to TDD in the process and implement all of the small things needed using TDD and allow the final behaviour to be verified by Cucumber. I will not use TDD in this small example, but BDD and TDD can and should be used together. They are not just friends, they are the same thing with possibly a slight difference in how they taste. The similarity is the same as with ice cream, an ice cream is an ice cream. But vanilla ice cream and chocolate ice cream taste differently and fits best in different situations.

An implementation of the steps may look like this:

src/test/java/se/thinkcode/geecon/StepDefinitions.java

package se.thinkcode.geecon;

import cucumber.api.java.en.Given;
import cucumber.api.java.pl.Kiedy;
import cucumber.api.java.pl.Mając;
import cucumber.api.java.pl.Wtedy;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;

public class StepDefinitions {
    private Belly belly;

    @Mając("^(\\d+) ogórki w brzuchu$")
    public void ogórki_w_brzuchu(int cukes) throws Throwable {
        belly = new Belly();
        belly.eat(cukes);
    }

    @Kiedy("^odczekam (\\d+) godzinę$")
    public void odczekam_godzinę(int waitingTime) throws Throwable {
        belly.waitAWhile(waitingTime);
    }

    @Wtedy("^mój brzuch zacznie (.*)$")
    public void mój_brzuch_zacznie_burczeć(String expectedBellySound) throws Throwable {
        String actualSound = belly.getSound();
        assertThat(actualSound, is(expectedBellySound));
    }
}

This implementation requires a class called Belly. This belly should be feed a lot of cukes and growl after a while.

The belly will be created in the first step, the setup of the system under test. That is the given step. I need access to it later so I will store it in a variable.

The belly will then be used in the second step, the when step.

The behaviour will finally be verified in the last step, the then step.

Implement the production code

The last piece of this puzzle is the actual implementation of Belly. A minimal implementation that is sufficient for now is:

src/main/java/se/thinkcode/geecon/Belly.java

package se.thinkcode.geecon;

public class Belly {
    private int cukes;
    private int waitingTime;

    public void eat(int cukes) {
        this.cukes = cukes;
    }

    public void waitAWhile(int waitingTime) {
        this.waitingTime = waitingTime;
    }

    public String getSound() {
        if (cukes > 41 && waitingTime >= 1) {
            return "burczeć";
        }

        return "";
    }
}

This is a minimalistic implementation but it is sufficient for us to get started with something that actually works on our quest for world domination.

File organization

The files I have used for in this example are organized like this:

example
|-- pom.xml
`-- src
    |-- main
    |   `-- java
    |       `-- se
    |           `-- thinkcode
    |               `-- geecon
    |                   `-- Belly.java
    `-- test
        |-- java
        |   `-- se
        |       `-- thinkcode
        |           `-- geecon
        |               |-- RunCukesTest.java
        |               `-- StepDefinitions.java
        `-- resources
            `-- se
                `-- thinkcode
                    `-- geecon
                        `-- belly.feature

If you are interested, feel free to implement this example and play around with it.

Conclusion

It is not very hard to implement something that will parse an example and execute it. There is no magic here. Some clever programming and usage of regular expressions created this framework that is available for us to do magic with.

Acknowledgments

I would like to thank Malin Ekholm and Alexandru Bolboaca for proofreading. I would also like to thank Piotr Kiernicki for helping me with the translation to Polish.

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