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.
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.
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.
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.
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.
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.
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.
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.
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.