JBehave Yourself
By Tim Dalton, OCI Senior Software Engineer
June 2010
Introduction
JBehave provides a framework for doing Behavior-Driven Development (BDD) on the Java platform. Behavior-Driven Development is an extension of Test-Driven Development (TDD) that focuses on facets of behavior rather verifying 'units' whether they be methods, classes or other. The emphasis is on specification rather than verification.
One goal of BDD is to provide a ubiquitous language for specifying expected behavior that can be executed as part of an acceptance test and can be understood by developers and stakeholders alike. Therefore, BDD tests have identifiable and verifiable business value.
The form that the specification language takes can vary across BDD frameworks. Many favor a Domain Specific Language (DSL) implemented using plain text while others use an internal DSL formed from features of a host language. This abstraction layer, especially for those using plain text, makes it easier to de-couple the specification from the underlying implementation. Code-based specifications are likely to appeal to other developers since features of a programming language are available such as binding repeated constants to some kind of symbol. Plain text specifications are more business-user friendly since no understanding of a programming language is needed.
Approach of JBehave
JBehave uses a natural language stored in a text file. The contents of one such specification file is also referred to as a story. The top of a JBehave specification is an optional description that can contain narrative phrases beginning with 'As a
', 'I want to
' or 'In order to
'. These contents are there for documentation purposes only and do not alter the behavior of JBehave in any way.
Stories in JBehave are composed of scenarios indicated by a phrase beginning with 'Scenario:
'. Scenarios employ three key words, Given
, When
and Then
, at the beginning of phrases within the scenario. These keywords mirror the Arrange-Act-Assert pattern often used in unit testing. Phrases beginning with these keywords are also referred to as steps.
Below is an example scenario for a hypothetical user interface:
Scenario: Unsuccessful Login
Given the user has entered username 'foo'
When the Login button is pressed
Then an alert dialog 'Please provide a password' is displayed
The 'Scenario:
' keyword can be followed by free form text, including new lines, until a line that begins with one of the keywords, Given
, When
or Then
, is reached.
Comments can be nested within the Given
, When
and Then
steps starting with '!--
'. Such comments are terminated by the end of the line just like '//
' comments in Java.
There is also supplemental keyword, And
, that is used to compose multiple instances of the other three.
Example using And
keyword and a comment:
Scenario: Successful Login
Given the user has entered username 'foo'
And the user has entered password 'bar' !-- comment
When the Login button is focused
And enter key is pressed
Then a page with title 'Main Page' is displayed
And text 'Welcome foo' is visible
Use of And
allows easy composition of various permutations of test arrangements in a natural language readable by humans.
JBehave Scenarios and Steps
JBehave executes textual steps found in scenarios using classes called Steps. Such classes can extend a default implementation of a Steps
class within JBehave or can be POJO's (Plain Old Java Objects). Within these classes, there are annotated methods that are invoked when they match a step in a scenario.
Multiple instances of objects implementing steps can be registered with an instance of Scenario
. Such instances are evaluated to see if there is a matching method. The Scenario
class has a one-to-one mapping to the text file containing the story.
Example: Bowling Behavior
To demonstrate the basics of JBehave, the behavior of a simple function that calculates bowling scores is going to be validated here.
First Scenario:
Scenario: No score after one roll
Given a strike is bowled
When scores are tallied
Then there should be no score at frame 1
The default method of mapping Scenario
classes to file names converts the camel case class name to an underscore separated file name with no extension. This file is required to be in the same package as the Scenario
class. This mapping scheme can be customized by providing different implementations of ScenarioDefiner
and ScenarioNameResolver
interfaces within JBehave.
Scenarios
Here is a Scenario class that maps to a story file using the defaults:
- package com.ociweb.jnb.jun2010.java.bowling
-
- public class BowlingScenario extends Scenario {
- public BowlingScenario() { super(); }
- }
This story for the example above will need to on the classpath in this file, "./com/ociweb/jnb/jun2010/java/bowling/bowling_scenario"
.
Scenario
is a descendant of junit.framework.TestCase
, so it can be run just like any JUnit test. JUnit will execute a testScenario
method implemented by Scenario
that will run all scenarios in the file. When the above scenario is run it will produce console output like:
(tfd/java/bowling/bowling_scenario)
Scenario: No score after one roll
Given a strike is bowled (PENDING)
When scores are tallied (PENDING)
Then there should be no score at frame 1 (PENDING)
When a step is (PENDING)
, it indicates that no matching handler method for it was found in any of the Steps instances associated with the Scenario
. Here, no Steps instances have been created for the Scenario
so all the steps in the scenario will indicate as being (PENDING)
. Steps that have been implemented but depend on a pending step will indicate (NOT PERFORMED)
. The default behavior of JBehave is to not fail the test for any pending step, but this can be changed via configuration.
Steps
Here is an example of a Steps class with an annotation to handle the 'Given a strike is bowled
' step above:
- public class BowlingSteps extends Steps {
- private ArrayList<Integer> rolls;
-
- @BeforeScenario
- public void resetRolls() {
- rolls = new ArrayList<Integer>();
- }
-
- @Given("a strike is bowled")
- public void aStrikeIsBowled() {
- rolls.add(10);
- }
The @BeforeScenario
annotation is very similar to a JUnit @Before
annotation. It indicates that a method is to be executed when the 'Scenario:
' keyword is encountered in the text specification in a story. There is also an @AfterScenario
annotation to invoke a method after the scenario is complete. To invoke methods at the beginning of a story or upon its completion, use the @BeforeStory
or @AfterStory
annotation. Instances of Steps classes are reused by the Scenario
object and therefore stateful. Care must be taken to reset the object to a known state before executing steps within a scenario.
The @Given
annotation indicates that a method is invoked when a Given
step is encountered that matches its value.
An instance of a Steps class can be be associated with the Scenario
in various ways. Here a single instance is added via a constructor parameter:
public BowlingScenario() { super(new BowlingSteps()); }
Executing the BowlingScenario
test will now show that the Given
step is no longer (PENDING)
:
(tfd/java/bowling/bowling_scenario)
Scenario: No score after one roll
Given a strike is bowled
When scores are tallied (PENDING)
Then there should be no score at frame 1 (PENDING)
A few more methods need to be implemented in the BowlingSteps
class to be able to run the scenario completely. First, a field to hold the result of the evaluation of the rolls is needed along with a @BeforeScenario
method to reset its value:
- // Validation contains two disjoint types, one indicates
- // failure and other indicates success
- private Validation<InvalidPinCountException, ArrayList<Integer>> scoreResult;
- ...
- @BeforeScenario
- public void resetScoreResult() {
- scoreResult = null;
- }
Here are the methods that match the When
and Then
steps:
- @When("scores are tallied")
- public void scoreAreTallied() {
- try {
- scoreResult = Validation.success(new ScoreBowlingGame().f(rolls));
- } catch (InvalidPinCountException ex) {
- scoreResult = Validation.fail(ex);
- }
- }
-
- @Then("there should be no score at frame $frame")
- public void noScoreAtFrame(Integer frame) {
- assertThat(scoreResult.isSuccess() &&
- scoreResult.success().length() < frame, is(true));
- }
Assertions in the above example are ones from the Hamcrest project. JBehave has built-in support for Hamcrest but allows the use of other assertion libraries.
Step Arguments
JBehave matches steps by converting the text value of the annotation to a regular expression. Argument specifications are words that begin with '$
' and are replaced with '(.*)
' to form regular expression groups that can be extracted. For the previous example, the contents of the @Then
annotation above would become 'there should be no score at frame (.*)
' which allows the argument for $frame
to be extracted.
In the example above, JBehave will convert the argument into an Integer. There are built-in converters for String
and child classes of Number
. Comma-delimited text can be matched to a java.util.List
of String
and Number
descendants as well. Custom converters can be registered with JBehave during scenario configuration.
By default, arguments are matched in the order they appear in the specification step. Example:
- @Then("the score at frame $frame should be $score")
- public void scoreAtFrameShouldBe(Integer frame, Integer score) {
- assertThat(scoreResult.isSuccess(), is(true));
- assertThat(scoreResult.success().get(frame - 1), is(score));
- }
The names of the method or parameters do not play a role in matching steps. It is still a good idea to have the parameter names match the argument names in the annotation and have a method name congruent with the function of the step.
Step Aliases
When multiple steps with the same meaning need to be matched to a single method, JBehave provides @Alias
and @Aliases
annotations. Example:
Scenario: No score after spare
Given an 8 count is bowled and spare is made
And a 7 count is bowled and only 2 are picked
When scores are tallied
Then the score at frame 1 should be 17
And the score at frame 2 should be 26
Methods to handle steps:
- @Given("an $count count is bowled and spare is made")
- @Alias("a $count count is bowled and spare is made")
- public void countIsBowledAndSpare(Integer count) {
- rolls.add(count);
- rolls.add(10 - count);
- }
-
- @Given("an $count count is bowled and only $next is picked")
- @Aliases(values = {
- "an $count count is bowled and only $next are picked",
- "a $count count is bowled and only $next is picked",
- "a $count count is bowled and only $next are picked"
- })
- public void countIsBowledAndSomePicked(Integer count, Integer next) {
- rolls.add(count); // add appends a value to a ArrayList
- rolls.add(next);
- }
The @Alias
and @Aliases
annotations are used to support better grammar here.
Named Arguments
If there is a need to match step arguments in a different order than how they appear in the method parameter list, the @Named
annotation can be used to map arguments to parameters by name. Example:
Scenario:
Given an 8 count is bowled and spare is made
And a 7 count is bowled and only 2 are picked
When scores are tallied
Then the score at frame 1 should be 17
And 26 the score at frame 2 should be !-- says Yoda
Here is the updated method:
- @Then("the score at frame $frame should be $score")
- @Alias("$score the score at frame $frame should be")
- public void scoreAtFrameShouldBe(@Named("frame") Integer frame,
- @Named("score") Integer score) {
- assertThat(scoreResult.isSuccess(), is(true));
- assertThat(scoreResult.success().get(frame - 1), is(score));
- }
The @Named
annotation overrides the default behavior of matching arguments to method parameters based on position.
Tabular Data
To prevent the need to duplicate scenarios in the specification for many different data points, JBehave supports Table Examples. Table Examples allows scenarios to be executed repeatedly using data specified in a tabular format in the story text. Example:
Scenario: Table
Given rolls are <rolls>
When scores are tallied
Then scores should be <scores>
Examples:
|rolls |scores
|9- | 9
|9/9 | 19
|X X 9 | 29
|X X X | 30
|X X X 8 | 30 58
|X X X 81 | 30 58 77 86
|X X X X X X X X X XXX | 30 60 90 120 150 180 210 240 270 300
|X X X X X X X X X XX9 | 30 60 90 120 150 180 210 240 270 299
|X X X X X X X X X XX- | 30 60 90 120 150 180 210 240 270 290
|9/X X X X X X X X XXX | 20 50 80 110 140 170 200 230 260 290
|X X X X X X X X X X9/ | 30 60 90 120 150 180 210 240 269 289
|X X X X X X X X X X9- | 30 60 90 120 150 180 210 240 269 288
|X X X X X X X X X X81 | 30 60 90 120 150 180 210 240 268 287
|X -/X X X X X X X XXX | 20 40 70 100 130 160 190 220 250 280
|X --X X X X X X X XXX | 10 10 40 70 100 130 160 190 220 250
|X 9/X 9/X 9/X 9/X 9/X | 20 40 60 80 100 120 140 160 180 200
|-------------------- | 0 0 0 0 0 0 0 0 0 0
The first row of a Table Example is interpreted as the header row that specifies the arguments used to run the scenario for each row in the table.
The rolls
column can be parsed from a notation that resembles those used in bowling and the scores
can be split from a space delimited string:
- @Given("rolls look like $rolls")
- @Alias("rolls are <rolls>")
- public void rollsLookLike(@Named("rolls") String rollString) {
- rolls = new ArrayList<Integer>();
- char[] rollChars = rollString.toCharArray();
- Integer lastRoll = null;
- for (int i=0; i < rollChars.length; i++) {
- if (rollChars[i] == 'X') {
- lastRoll = 10;
- } else if (rollChars[i] >= '0' && rollChars[i] <= '9') {
- lastRoll = Integer.parseInt("" + rollChars[i]);
- } else if (rollChars[i] == '/') {
- lastRoll = 10 - lastRoll;
- } else if (rollChars[i] == '-') {
- lastRoll = 0;
- } else {
- lastRoll = null;
- }
- if (lastRoll != null) { rolls.add(lastRoll); };
- }
- }
-
- @Then("scores should be <scores>")
- public void scoreShouldBe(@Named("scores") String expectedScores) {
- assertThat(scoreResult.isSuccess(), is(true));
- String[] scoreArray = expectedScores.split(" +");
- assertThat(scoreResult.success().length(), is(scoreArray.length));
- for (int i=0; i < scoreArray.length; i++) {
- assertThat(scoreResult.success().get(i), is(new Integer(scoreArray[i])));
- }
- }
Table examples require arguments to be enclosed in angle brackets (<>
) in the annotation value and use of the @Named
annotation.
When a Scenario
using a Table Example is executed, output for each row in the table is generated with the values substituted in the steps of the scenario like below:
Example: {scores=9, rolls=9-}
Given rolls are 9-
When scores are tallied
Then scores should be 9
Example: {scores=19, rolls=9/9}
Given rolls are 9/9
When scores are tallied
Then scores should be 19
Example: {scores=29, rolls=X X 9}
...
Reporting
By default, JBehave outputs only to the console (System.out
). Scenarios can configured to output in multiple formats and to be directed to a file. Format supported include the following:
Text
This is the default output format used in the output examples above. By default, only steps that are not runnable or fail are output. It can be configured such that output for all steps are echoed to the console or out to a file in plain text format.
HTML
Reports in HTML format simply use <div>
tags with various values for their class
attributes. Table Examples are converted to HTML <table>
elements.
Output for all steps is generated.
Example:
- <div class="scenario">
- <h2>Scenario: Too many rolls such be ignored</h2>
- <div class="step successful">Given rolls look like XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</div>
- <div class="step successful">When scores are tallied</div>
- <div class="step successful">Then the score at frame 10 should be 300</div>
- </div>
- <div class="scenario">
- <h2>Scenario: Table</h2>
- <div class="examples">
- <h3>Examples:</h3>
- <div class="step">Given rolls are <rolls></div>
- <div class="step">When scores are tallied</div>
- <div class="step">Then scores should be <scores></div>
- <table>
- <thead>
- <tr>
- <th>rolls</th><th>scores</th></tr>
- </thead>
- <tbody>
- <tr>
- <td>9-</td><td>9</td></tr>
- <tr>
- <td>9/9</td><td>19</td></tr>
- ...
XML
Here elements such an story
, scenario
, step
and example
are used to tag results and again output for all steps is generated:
- <scenario keyword="Scenario:" title="Too many rolls should be ignored">
- <step outcome="successful">Given rolls look like XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</step>
- <step outcome="successful">When scores are tallied</step>
- <step outcome="successful">Then the score at frame 10 should be 300</step>
- </scenario>
- <scenario keyword="Scenario:" title="Table">
- <examples keyword="Examples:">
- <step>Given rolls are <rolls></step>
- <step>When scores are tallied</step>
- <step>Then scores should be <scores></step>
- <parameters>
- <names><name>rolls</name><name>scores</name></names>
- <values><value>9-</value><value>9</value></values>
- <values><value>9/9</value><value>19</value></values>
Statistics Report
Not a report format, but rather it is a summary of statistics gathered during execution of a JBehave story. Example:
#org.jbehave.scenario.reporters.StatisticsScenarioReporter
#Wed May 19 20:08:34 CDT 2010
stepsFailed=0
examples=17
scenariosFailed=0
scenarios=28
givenScenarios=0
stepsPending=0
stepsIgnorable=0
steps=141
stepsNotPerformed=0
stepsSuccessful=141
Other Features
Here is a summary of JBehave features not covered here:
Table Parameters
When tabular data is required to passed as an argument to a step, JBehave supports parameters using the same '|
' delimited format used by Table Examples. A parameter of class ExampleTable
is passed into the matching method. The ExampleTable
provides a List
where items of the List
represents the rows and Map
elements allow the lookup of data values based on header names.
Non-English Language support
Specifications can be written in languages other than English by using an I18n locale file and configuring the Scenario
to use it.
Plugins for Dependency Injection Frameworks
JBehave support configuration of Steps using the popular dependency injection frameworks for the Java platform. This allows the injection of mocks and stubs of external interfaces required for testing. Frameworks include:
- Spring
- Guice
- PicoContainer
Ant and Maven integration
JBehave has Ant tasks ScenarioRunnerTask
and ReportRendererTask
for running the scenarios and rendering reports. For Maven, the jbehave-maven-plugin
plugin can be used. The Maven plugin provides goals run-scenarios
and render-reports
for those same purposes.
Web Integration
Packaged separately from JBehave, JBehave Web provides a web-integration extension that consists of to distinct parts:
- Web Runner
The JBehave Web Runner web component that allows users to enter and execute textual scenarios via a web browser.
- Selenium Integration
Provides a
SeleniumSteps
class that can be extended to develop scenarios that use Selenium for testing using an automated web browser.
Given Scenarios
Provides a mechanism for stories to include scenarios from other files. A GivenScenario
can be used within a Scenario
before the first Given
step.
Summary
JBehave provides a lot of benefits while being relatively simple. The ability to compose arrangements, actions and assertions using plain text provides great flexibility. Use of frameworks like JBehave facilitate the 'tests as documentation' ideal valued by Agile programming practices.
Source Code
All source examples use Maven 2 and are set up to run JBehave scenarios as part of the test
phase.
-
Bowling Samples:
- Java version: Jun2010-java-bowling.zip
- Scala version: Jun2010-scala-bowling.zip
Bowling program and JBehave fixtures written in Scala. JBehave works with Scala seamlessly.
- Sample Application integrating JBehave with Mockito: Jun2010-jbtrek.zip
A more complex example that demonstrates how JBehave can be used in the 'Enterprise'.
References
- [1] JBehave
http://jbehave.org - Wikipedia
- [2] Behavior Driven Development
http://en.wikipedia.org/wiki/Behavior_Driven_Development - [3] Test Driven Development
http://en.wikipedia.org/wiki/Test-driven_development - [4] Domain Specific Language
http://en.wikipedia.org/wiki/Domain-specific_language
- [2] Behavior Driven Development
- [5] Hamcrest Project
http://code.google.com/p/hamcrest/ - [6] Selenium
http://seleniumhq.org/ - [7] Mockito
http://code.google.com/p/mockito/ - [8] Fixtures for Easy Software Testing (FEST)
http://fest.easytesting.org/