Test drive an implementation using an Executable Specification - revisited

Filed under: Cucumber, Java, Test automation, — Tags: Acceptance test, Acceptance test driven development, Agile Cambridge 2012, Cucumber-jvm, Specifications by example — Thomas Sundberg — 2012-09-28

An example is perhaps the best way to describe something. Concrete examples are easier to understand than abstract descriptions.

I will show how Cucumber-JVM can be used to specify an example and how the example can be connected to the system under test, SUT. The example can then be executed using any IDE or using Maven.

The example

The example I will use is about car maintenance. A car with an empty fuel tank need to be refueled. We will develop some code that will solve this problem. What we actually will implement is a simple adding function, but that is beside the point.

File structure

Before you start building the example, I need to show the file structure this example lives in. This is almost big design upfront, but hopefully you can bear with me while I do that.

example
|-- acceptance
|   |-- pom.xml
|   `-- src
|       `-- test
|           |-- java
|           |   `-- se
|           |       `-- waymark
|           |           `-- example
|           |               |-- RunCukesTest.java
|           |               `-- steps
|           |                   `-- FuelCarSteps.java
|           `-- resources
|               `-- se
|                   `-- waymark
|                       `-- example
|                           `-- CarMaintenance.feature
|-- pom.xml
`-- product
    |-- pom.xml
    `-- src
        `-- main
            `-- java
                `-- se
                    `-- waymark
                        `-- example
                            `-- Car.java

Given this file structure, you should be able to re-create this example.

The feature

Lets start with a feature. This is possibly the starting point for a very large project.

acceptance/src/test/resources/se/waymark/example/CarMaintenance.feature

Feature: Daily car maintenance
  Cars need maintenance


  Scenario: Fuelling
    Given a car with 10 litres of fuel in the tank
    When you fill it with 50 litres of fuel
    Then the tank contains 60 litres

This feature is divided into two parts.

It starts with a headline and a description. This is background information. It tells us why we should bother creating this feature.

The introduction is followed by a scenario. A feature is made out of many scenarios. In this example there is just one scenario.

The scenario is then divided into three parts.

Glue code

The feature above is not possible to run on its own. It need som glue to connect with the system that should be tested. The glue is divided in two parts. First there is a JUnit class that will connect the feature to a runner. It may look like this:

acceptance/src/test/java/se/waymark/example/RunCukesTest.java

package se.waymark.example;

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

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

The test class is annotated with an annotation that defines that this class should be executed with a JUnit runner called 'Cucumber.class'.

@RunWith(Cucumber.class)

This annotation will enable us to execute this feature both in a IDE and with any build tool. With this in place, you can debug a single feature as well as enjoy continuous integration using tools like Maven. The JUnit runner will search for features in the classpath so it is important that they are located in the same package as the test class or in a subpackage to the test class.

Then there are the step definitions that actually connects the feature to the system under test. It may look like this:

acceptance/src/test/java/se/waymark/example/steps/FuelCarSteps.java

package se.waymark.example.steps;

import cucumber.annotation.en.Given;
import cucumber.annotation.en.Then;
import cucumber.annotation.en.When;
import se.waymark.example.Car;

import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;

public class FuelCarSteps {
    private Car car;

    @Given("^a car with (\\d*) litres of fuel in the tank$")
    public void createCar(int initialFuelLevel) {
        car = new Car(initialFuelLevel);
    }

    @When("^you fill it with (\\d*) litres of fuel$")
    public void addFuel(int addedFuel) {
        car.addFuel(addedFuel);
    }

    @Then("^the tank contains (\\d*) litres$")
    public void checkBalance(int expectedFuelLevel) {
        int actualFuelLevel = car.getFuelLevel();
        assertThat(actualFuelLevel, is(expectedFuelLevel));
    }
}

It consists of three methods. One method for each step

Every step definition is annotated with a regular expression. If the regular expression contains capture groups, a pair of parenthesis, the matches from these groups will be passed to the step definition method. The captured string will automatically be converted to the declared parameter type.

The step definitions and the test class must be separated. It means that there will be an empty test class. This is the desired behaviour of Cucumber.

Production code

With a feature and step definitions in place, it is time to develop the production code. This is the simplest solution that could work. It may even be seen as so small that it will not suffice. But it is sufficient for this starting point of our world domination product.

product/src/main/java/se/waymark/example/Car.java

package se.waymark.example;

public class Car {
    private Integer fuelLevel;

    public Car(int initialFuelLevel) {
        fuelLevel = initialFuelLevel;
    }

    public void addFuel(int addedFuel) {
        fuelLevel = fuelLevel + addedFuel;
    }

    public int getFuelLevel() {
        return fuelLevel;
    }
}

Maven poms

The final glue needed to be able to execute this example are three Maven poms. I use Maven to avoid all problems with downloading dependencies. Maven also integrates very well with the editor of my choice, IntelliJ IDEA.

Lets start from the top of our file hierarchy, the root pom for the entire example.

example/pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>se.waymark.cucumber</groupId>
    <artifactId>example</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>
    <modules>
        <module>product</module>
        <module>acceptance</module>
    </modules>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>
    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.10</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

It connects the two modules and defines that JUnit should be available in every module during the test phase.

The next pom we need is the pom for the production code example/product/pom.xml

example/product/pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project>
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>se.waymark.cucumber</groupId>
        <artifactId>example</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <artifactId>product</artifactId>
</project>

It doesn't do anything more than defines the module so it can be built.

The final pom that is needed is one that retrieves the cucumber dependencies and connects the production code to the acceptance tests.

example/acceptance/pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project>
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>se.waymark.cucumber</groupId>
        <artifactId>example</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <artifactId>acceptance</artifactId>
    <dependencies>
        <dependency>
            <groupId>se.waymark.cucumber</groupId>
            <artifactId>product</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>info.cukes</groupId>
            <artifactId>cucumber-java</artifactId>
            <version>1.0.14</version>
        </dependency>
        <dependency>
            <groupId>info.cukes</groupId>
            <artifactId>cucumber-junit</artifactId>
            <version>1.0.14</version>
        </dependency>
    </dependencies>
</project>

Conclusion

It is not very difficult to implement an executable specification. We need a feature, some scenarios, step definitions and some wiring to connect the different parts. The feature is written using Gherkin, the step definitions and production code is written in Java and finally the wiring is done using Maven.

So Executable Specification or Specifications by Example isn't rocket science, it is definitely doable and is a a great complement to the unit tests that you should use when developing the product.

Acknowledgements

This post has been reviewed by some people who I wish to thank for their help

Thank you very much for your feedback!

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