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 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.
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.
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.
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.
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; } }
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>
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.
This post has been reviewed by some people who I wish to thank for their help
Thank you very much for your feedback!