Filed under: Cucumber, Requirements, Software development, Test automation, — Tags: BDD, Behaviour Driven Development, Behaviour Driven Development - BDD, Cucumber, Cucumber-jvm, Executable specifications, I T.A.K.E. Unconference 2014, Java, Living documentation, Maven, Test automation — Thomas Sundberg — 2014-05-29
This is the example I showed at the I T.A.K.E. Unconference 2014 in Bucharest. I created it for your convenience so you should be able to implement it yourself after the presentation.
Before we dive into the example, let me just recap what I am aiming for. I will show you how an example (or specification if you want) can be executed. The example is written in plain text and it is used as the basis for an execution. This example can later be relied upon for regression testing as well as living documentation.
I use Maven and Java in this example. If you don't have them installed, please install them before you continue.
There are five files involved in the example. They should live this directory structure:
example |-- pom.xml `-- src |-- main | `-- java | `-- se | `-- thinkcode | `-- itake | `-- Belly.java `-- test |-- java | `-- se | `-- thinkcode | `-- itake | |-- RunCukesTest.java | `-- StepDefinitions.java `-- resources `-- se `-- thinkcode `-- itake `-- belly.feature
Let us start by examining the feature file. This is the plain text file where the example is defined. It follows a specific syntax called Gherkin.
src/test/resources/se/thinkcode/itake/belly.feature
Feature: Belly Scenario: a few cukes Given I have 42 cukes in my belly When I wait 1 hour Then my belly should growl
A feature file must start with the keyword Feature and it should be followed by a description of the feature. This description could be long and be written on many lines. It could be a user story. The description could be followed of a user story. It is all up to you how it should look like. The only important thing here is that the description is descriptive and easy to understand.
Next important thing is the Scenario. It should describe the steps that will be executed for this example. Each step begins with any of the keywords Given/When/Then and is followed by the actual step.
This is all that is needed to write a scenario in a feature. A feature may consist of many scenarios where each scenario describe a certain usage example of the system. There is only one scenario in this small example, but you can add many more.
Some boilerplate code is needed to execute the example. I use a JUnit to execute Cucumber. It can be implemented as
src/test/java/se/thinkcode/itake/RunCukesTest.java
package se.thinkcode.itake; import cucumber.api.junit.Cucumber; import org.junit.runner.RunWith; @RunWith(Cucumber.class) public class RunCukesTest { }
This is a very small class that only defines that it should be executed by a JUnit runner that invokes Cucumber, The class name ends with Test. This means that the Surefire plugin in Maven will be able to pick it up and execute it. The feature file must be located in the same package as the runner. Cucumber will examine its classpath and search for all files with the suffix .feature and execute any scenarios it can find.
The steps needed to execute the example are not allowed to be implemented in this class. The steps needed must be implemented in other classes.
The third file needed in this example is a class that implements the actual steps that should be executed. I implement them in a utility class called StepDefinitions. This is the glue that will connect the feature files found with the actual system. My implementation looks like this:
src/test/java/se/thinkcode/itake/StepDefinitions.java
package se.thinkcode.itake; import cucumber.api.java.en.Given; import cucumber.api.java.en.Then; import cucumber.api.java.en.When; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; public class StepDefinitions { private Belly belly; private int waitingTime; @Given("^I have (\\d+) cukes in my belly$") public void i_have_cukes_in_my_belly(int cukes) throws Throwable { belly = new Belly(); belly.eat(cukes); } @When("^I wait (\\d+) hour$") public void i_wait_hour(int waitingTime) throws Throwable { this.waitingTime = waitingTime; } @Then("^my belly should (.*)$") public void my_belly_should_growl(String expectedSound) throws Throwable { String actualSound = belly.getSound(waitingTime); assertThat(actualSound, is(expectedSound)); } }
There are three methods defined. One for each step in the example above. Each method is annotated with a Gherkin keyword followed by a regular expression that should match the step in the feature. A regular expression group is used to find a parameter that should be picked up from the example. The first method is getting a digit that describes how many cukes that has been eaten. The steps must be located in the same package or a subpackage relative to the JUnit class that will execute Cucumber.
The methods needed to map steps to actual Java code may be implemented in many step definition files. It was convenient to use just one in this example, but if it had made sense I could have implemented the methods in many classes. All step methods are global and visible to Cucumber. This means that the functionality needed may be implemented in more than one class. Global functions may feel horrible but there is an cunning idea behind having them global. If you describe your system using the exact same words and mean two different areas of your system, you have a larger problem than global methods. You will need to address this so parts that does different things in your system actually is described differently.
I also need some production code that actually implements my belly. It is implemented in the class Belly.
src/main/java/se/thinkcode/itake/Belly.java
package se.thinkcode.itake; public class Belly { private int cukes; public void eat(int cukes) { this.cukes = cukes; } public String getSound(int waitingTime) { if (cukes > 41 && waitingTime >= 1) { return "growl"; } else { return "silent"; } } }
The final thing needed to complete the example is a Maven pom that will help us get the dependencies we want and run the test. A Maven pom that satisfies this requirement is:
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project> <modelVersion>4.0.0</modelVersion> <groupId>se.thinkcode</groupId> <artifactId>cucumber-itake-unconference-2014</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>info.cukes</groupId> <artifactId>cucumber-java</artifactId> <version>1.1.7</version> <scope>test</scope> </dependency> <dependency> <groupId>info.cukes</groupId> <artifactId>cucumber-junit</artifactId> <version>1.1.7</version> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> </dependencies> </project>
Finally run the example by executing
mvn test
in the root directory where pom.xml is located.
All dependencies will be downloaded the first time you run Maven. This may take some time.
This is a very small example, nothing more than a Hello World. It should be small enough so you are able to implement it without getting lost among all the details. It is at the same time large enough to show how you can execute a plain text example. To prove to yourself that the example actually is used, change the number of cukes eaten to 17 and re-run. This should fail. Change back to 42, re-run and watch it pass.