Automated Acceptance-Testing using JBehave - codecentric AG Blog

:

Introduction

Nowadays Agile Teams can choose from a wide variety of tools for automating Acceptence Tests. Thereby a lot of quite different concepts show up. FitNesse for example is using an integrated Wiki to organise testcases, while the Robot Framework is using keyword-driven test development. These are examples of two well-established tools in this area. These tools have a rich scope of operation, but therefore they also require a longer period of vocational adjustment.

In comparison JBehave seems to be very lightweight on first sight (and also on second sight). There is the description of the testcases in BDD-format on one side and the implementation of the required test functionality in Java on the other side. Both parts can be easily connected using Maven.

As this article is a bit longer a short overview on the topics that follow is given here:

  • Maven Configuration: Explains the required configuration for Maven to implement and execute tests using JBehave.
  • Testcases: Shows how testcases are written in JBehave using the BDD-format.
  • Implementation: Explains the required steps to implement the test functionality in Java that is matching the description of certain testcases.
  • Reporting: Showing the results of a test execution with JBehave.
  • Putting the pieces together: Maven, description of testcases and implementation of test functionality are glued together using certain naming and directory conventions.
  • Conclusion: How suitable is JBehave for automating Acceptance Tests? And for whom?
  • Download: Download the complete example as a ZIP-file.

Ship’s log supplement as of 16.06.2012: My collegue Andreas has written a very good article on the various configuration possibilities of JBehave.. This one could be surely of great help after starting with JBehave :-).

Maven Configuration

For projects that are developed using Java, JBehave has definitely the big advantage that the integration to the existing development environment of the team works very smoothly. This is especially the case if Maven is already used as the build tool. Of course this is working for other testing tools as well. But especially if these tools are not initially developed in and for Java this often gets more complicated than desired.

The following Maven configuration is sufficient to start with the development of Acceptance Tests with JBehave. Tests can be compiled, started and even artifacts required for the reporting can be downloaded with this:

<?xml version="1.0" encoding="UTF-8"?>
 
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
 
  <url>http://maven.apache.org</url>
  <modelVersion>4.0.0</modelVersion>
  <name>JBehaveDemo</name>
  <groupId>de.codecentric</groupId>
  <artifactId>jbehave-demo</artifactId>
  <packaging>jar</packaging>
  <version>1.0-SNAPSHOT</version>
 
  <dependencies>
    <dependency>
      <groupId>org.jbehave</groupId>
      <artifactId>jbehave-core</artifactId>
      <version>3.1.2</version>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.4</version>
    </dependency>
  </dependencies>
 
  <build>
    <plugins>
      <plugin>
        <groupId>org.jbehave</groupId>
        <artifactId>jbehave-maven-plugin</artifactId>
        <version>3.1.2</version>
        <executions>
          <execution>
            <id>run-stories-as-embeddables</id>
            <phase>integration-test</phase>
            <configuration>
              <includes>
                <include>**/*Scenarios.java</include>
              </includes>
              <ignoreFailureInStories>true</ignoreFailureInStories>
              <ignoreFailureInView>false</ignoreFailureInView>
           </configuration>
           <goals>
              <goal>run-stories-as-embeddables</goal>
           </goals>
         </execution>
       </executions>
     </plugin>
 
     <plugin> 
       <groupId>org.apache.maven.plugins</groupId> 
       <artifactId>maven-dependency-plugin</artifactId> 
       <executions> 
         <execution> 
            <id>unpack-jbehave-site-resources</id>
            <phase>generate-resources</phase> 
            <goals> 
               <goal>unpack</goal> 
            </goals> 
            <configuration> 
               <overwriteReleases>false</overwriteReleases> 
               <overwriteSnapshots>true</overwriteSnapshots> 
               <artifactItems> 
                  <artifactItem> 
                     <groupId>org.jbehave.site</groupId> 
                     <artifactId>jbehave-site-resources</artifactId> 
                     <version>3.1.1</version> 
                     <type>zip</type>
                     <outputDirectory> ${project.build.directory}/jbehave/view</outputDirectory> 
                   </artifactItem> 
                </artifactItems> 
            </configuration> 
         </execution> 
         <execution> 
            <id>unpack-jbehave-reports-resources</id>
            <phase>generate-resources</phase> 
            <goals> 
               <goal>unpack</goal> 
            </goals> 
            <configuration> 
              <overwriteReleases>false</overwriteReleases> 
              <overwriteSnapshots>true</overwriteSnapshots> 
              <artifactItems> 
                 <artifactItem> 
                   <groupId>org.jbehave</groupId> 
                   <artifactId>jbehave-core</artifactId> 
                   <version>3.1.2</version> 
                   <outputDirectory>${project.build.directory}/jbehave/view</outputDirectory> 
                   <includes>**\/*.css,**\/*.ftl,**\/*.js</includes> 
                 </artifactItem> 
               </artifactItems> 
             </configuration> 
           </execution> 
         </executions> 
       </plugin> 			
     </plugins>
  </build>
</project>

<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"><url>http://maven.apache.org</url> <modelVersion>4.0.0</modelVersion> <name>JBehaveDemo</name> <groupId>de.codecentric</groupId> <artifactId>jbehave-demo</artifactId> <packaging>jar</packaging> <version>1.0-SNAPSHOT</version><dependencies> <dependency> <groupId>org.jbehave</groupId> <artifactId>jbehave-core</artifactId> <version>3.1.2</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.4</version> </dependency> </dependencies><build> <plugins> <plugin> <groupId>org.jbehave</groupId> <artifactId>jbehave-maven-plugin</artifactId> <version>3.1.2</version> <executions> <execution> <id>run-stories-as-embeddables</id> <phase>integration-test</phase> <configuration> <includes> <include>**/*Scenarios.java</include> </includes> <ignoreFailureInStories>true</ignoreFailureInStories> <ignoreFailureInView>false</ignoreFailureInView> </configuration> <goals> <goal>run-stories-as-embeddables</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <executions> <execution> <id>unpack-jbehave-site-resources</id> <phase>generate-resources</phase> <goals> <goal>unpack</goal> </goals> <configuration> <overwriteReleases>false</overwriteReleases> <overwriteSnapshots>true</overwriteSnapshots> <artifactItems> <artifactItem> <groupId>org.jbehave.site</groupId> <artifactId>jbehave-site-resources</artifactId> <version>3.1.1</version> <type>zip</type> <outputDirectory> ${project.build.directory}/jbehave/view</outputDirectory> </artifactItem> </artifactItems> </configuration> </execution> <execution> <id>unpack-jbehave-reports-resources</id> <phase>generate-resources</phase> <goals> <goal>unpack</goal> </goals> <configuration> <overwriteReleases>false</overwriteReleases> <overwriteSnapshots>true</overwriteSnapshots> <artifactItems> <artifactItem> <groupId>org.jbehave</groupId> <artifactId>jbehave-core</artifactId> <version>3.1.2</version> <outputDirectory>${project.build.directory}/jbehave/view</outputDirectory> <includes>**\/*.css,**\/*.ftl,**\/*.js</includes> </artifactItem> </artifactItems> </configuration> </execution> </executions> </plugin> </plugins> </build> </project>

As said with this configuration all required Jar files are downloaded automatically and the tests can be started using the command: “mvn integration-test”. Of course we need some testcases and implementation of test functionality for this to work.

Testcases

JBehave supports the so-called BDD-format (Given/When/Then) for writing testcases. This format has the following syntax:

Given:    a static precondition
And:    another static precondition
And:   
When:    tested behaviour
And:    more tested behaviour
And:   
Then:    the result of the tested behaviour under the given preconditions
And:   

This format has the big advantage that it can be read quite easily also from non-technical people. This enables the team to establish a common understanding between the development team and members of the operating departments and/or customers.

Writing automated Acceptance Tests is a task that – in an ideal case – is always performed in close cooperation between the experts of the operating departments and the agile development team. This ensures that really all required compentences are available and that first results from the test execution can be analysed quickly together. A description of the testcases that is easily understood by all involved parties is of course a mandatory precondition for this to work.

In JBehave the testcases are written in co-called story-files. Thereby it is a meaningful convention that in one of these story-files all tests for one user story are described. It is already taken care of when writing user stories that those do not become too big. This ensures that the amount of testcases in these story-files are as well kept at a reasonable level. In addition this is supported by the clear structure of the story-files. Testcases are by the way called “scenarios” in JBehave.

To keep this example as simple as possible the tests are written for the existing stack implementation of Java. This way the use case is very simple and the focus can be fully put on the concepts and correlations of JBehave.

Narrative:
In order to develop an application that requires a stack efficiently
As a development team
I would like to use an interface and implementation in Java directly
 
 
Scenario:  Basic functionality of a Stack
 
Given an empty stack
When the string Java is added
And the string C++ is added
And the last element is removed again
Then the resulting element should be Java
 
Scenario:  Stack search
 
Given an empty stack
When the string Java is added
And the string C++ is added
And the string PHP is added
And the element Java is searched for
Then the position returned should be 3

Narrative: In order to develop an application that requires a stack efficiently As a development team I would like to use an interface and implementation in Java directlyScenario: Basic functionality of a Stack Given an empty stack When the string Java is added And the string C++ is added And the last element is removed again Then the resulting element should be JavaScenario: Stack search Given an empty stack When the string Java is added And the string C++ is added And the string PHP is added And the element Java is searched for Then the position returned should be 3

At the beginning of a story-file it is possible to explain the tested user story once again. This is happening after the keyword Narrative:. Of course it makes sense to repeat here – a hopefully already existing description – of the user story. But as this text is anyway completely ignored with respect to the testing functionality, any text can be put here in the end.

Especially for tests written in the BDD-format it makes much sense to write them in your native language. In the example above the tests are written in English language as this is supported out-of-the-box by JBehave. But it is possible to translate the used keywords (Given/When/Then) into other languages as well doing some additional coding in Java.

After this any amount of testcase (=Scenarios) can be written. Those are always started with the keyword Scenario: followed by a brief description of this testcase. Then the actual definition of the testcase is given.

Implementation

Mapping the description of the testcases to the corresponding implementation is really simple. Each row in the description of the testcases (in JBehave those are called steps) is mapped to a corresponding method in a Java class. The methods are marked with the proper annotations defined by JBehave. Passing parameters is handled using a naming convention ($variable) in the annotations. Of course those must then be as well defined as parameters in the method definitions.

package de.codecentric.jbehave;
 
import java.util.Stack;
import org.jbehave.core.annotations.*;
import org.jbehave.core.embedder.Embedder;
import junit.framework.Assert;
 
public class StackStories extends Embedder{
 
    private Stack<String> testStack;
    private String searchElement;
 
    @Given("an empty stack")
    public void anEmptyStack() {
        testStack = new Stack<String>();
    }
 
    @When("the string $element is added")
    public void anElementIsAdded(String element) {
        testStack.push(element);
    }
 
    @When("the last element is removed again")
    public void removeLastElement() {
        testStack.pop();
    }
 
    @When("the element $element is searched for")
    public void searchForElement(String element) {
        searchElement = element;
    }
 
    @Then("the resulting element should be $result")
    public void theResultingElementShouldBe(String result) {
    	Assert.assertEquals(testStack.pop(), result);
    }
 
    @Then("the position returned should be $pos")
    public void thePositionReturnedShouldBe(int pos) {
    	Assert.assertEquals(testStack.search(searchElement), pos);
    }
}

package de.codecentric.jbehave;import java.util.Stack; import org.jbehave.core.annotations.*; import org.jbehave.core.embedder.Embedder; import junit.framework.Assert;public class StackStories extends Embedder{private Stack<String> testStack; private String searchElement; @Given("an empty stack") public void anEmptyStack() { testStack = new Stack<String>(); } @When("the string $element is added") public void anElementIsAdded(String element) { testStack.push(element); } @When("the last element is removed again") public void removeLastElement() { testStack.pop(); }@When("the element $element is searched for") public void searchForElement(String element) { searchElement = element; } @Then("the resulting element should be $result") public void theResultingElementShouldBe(String result) { Assert.assertEquals(testStack.pop(), result); } @Then("the position returned should be $pos") public void thePositionReturnedShouldBe(int pos) { Assert.assertEquals(testStack.search(searchElement), pos); } }

The class shown above implements the so-called steps from the test descriptions. To be able to execute the tests from a story-file an additional Java class is required. This class is mapped to a story file using some naming conventions and furthermore some common settings (like reporting) are defined in this class:

package de.codecentric.jbehave;
 
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
 
import org.jbehave.core.configuration.Configuration;
import org.jbehave.core.configuration.MostUsefulConfiguration;
import org.jbehave.core.io.LoadFromRelativeFile;
import org.jbehave.core.junit.JUnitStory;
import org.jbehave.core.reporters.StoryReporterBuilder;
import org.jbehave.core.reporters.StoryReporterBuilder.Format;
import org.jbehave.core.steps.CandidateSteps;
import org.jbehave.core.steps.InstanceStepsFactory;
import org.junit.Test;
 
public class StackScenarios extends JUnitStory {
 
    @Override
    public Configuration configuration() {
        URL storyURL = null;
        try {
            // This requires you to start Maven from the project directory
            storyURL = new URL("file://" + System.getProperty("user.dir")
                    + "/src/main/resources/stories/");
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
        return new MostUsefulConfiguration().useStoryLoader(
                new LoadFromRelativeFile(storyURL)).useStoryReporterBuilder(
                new StoryReporterBuilder().withFormats(Format.HTML));
    }
 
    @Override
    public List<CandidateSteps> candidateSteps() {
        return new InstanceStepsFactory(configuration(), new StackSteps())
                .createCandidateSteps();
    }
 
    @Override
    @Test
    public void run() {
        try {
            super.run();
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
}

package de.codecentric.jbehave;import java.net.MalformedURLException; import java.net.URL; import java.util.List;import org.jbehave.core.configuration.Configuration; import org.jbehave.core.configuration.MostUsefulConfiguration; import org.jbehave.core.io.LoadFromRelativeFile; import org.jbehave.core.junit.JUnitStory; import org.jbehave.core.reporters.StoryReporterBuilder; import org.jbehave.core.reporters.StoryReporterBuilder.Format; import org.jbehave.core.steps.CandidateSteps; import org.jbehave.core.steps.InstanceStepsFactory; import org.junit.Test;public class StackScenarios extends JUnitStory {@Override public Configuration configuration() { URL storyURL = null; try { // This requires you to start Maven from the project directory storyURL = new URL("file://" + System.getProperty("user.dir") + "/src/main/resources/stories/"); } catch (MalformedURLException e) { e.printStackTrace(); } return new MostUsefulConfiguration().useStoryLoader( new LoadFromRelativeFile(storyURL)).useStoryReporterBuilder( new StoryReporterBuilder().withFormats(Format.HTML)); }@Override public List<CandidateSteps> candidateSteps() { return new InstanceStepsFactory(configuration(), new StackSteps()) .createCandidateSteps(); }@Override @Test public void run() { try { super.run(); } catch (Throwable e) { e.printStackTrace(); } } }

The API of JBehave is offering some more possibilities, but these classes are sufficient to start testing. Unfortunately is the quality of the JavaDoc documentation of the API rather poor.

It should be noted that it is not mandatory to have an implementation for every step in the story-file right away. JBehave can handle situations where steps are missing and marks them then per default as “pending” in the report. This way tests can be written before the actual implementation must be there. The behaviour of JBehave with respect to this situation is configurable and if desired a missing step implementation can also result in an error.

Obviously it is very helpful being able to execute the tests directly from an IDE (e. g. Eclipse). This is working with JBehave very easily thanks to this unimposing method from the above shown class:

...
    @Override
    @Test
    public void run() {
        try {
            super.run();
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
...

... @Override @Test public void run() { try { super.run(); } catch (Throwable e) { e.printStackTrace(); } } ...

With this tests can be directly executed with JUnit. I found this possibility by chance when trying things out as it was not stated explicitly in the documentation (or I have just not seen it). But this way tests can for example be started in the debugger quite nicely.

Reporting

The following screenshot shows the report generated by JBehave when executing the tests from this example:

The amount of information is not really overwhelming but sufficient. Especially the – in this kind of report – important information about the execution date and time is shown in the footer of the report. The two links at the end of the row are leading to the definition of the related story-file and a page displaying the execution statistics directly. The whole configuration of the reporting has for sure still some possibilities to explore. But for this overview this should be enough for the time being.

The report itsself if located in the directory target/jbehave/view of the Eclipse workspace after execution. It has the name reports.html. To make the report look pretty additional style information is required. And as we luckily have already made the corresponding entries in the POM-file we can download the required files easily using Maven:

mvn generate-resources

Afterwards there should be additional directories (ftl, images …) showing up below the directory target/jbehave/view.

Putting the pieces together

There are three important parts for developing Acceptance Tests with JBehave:

  • Maven Configuration: Once this one is working it is more or less static.
  • Testcases: A new story-file is added for every new user story. Of course existing scenarios might be changed or extended in the course of a project.
  • Implementation: The Java class required for executing the tests is more or less static. To add more stories it would make sense here to consider a generic super class. The implementation of the test functionality as such is happening in parallel to writing the testcases. These classes contain the real logic of the tests.

There are some naming conventions used in JBehave to glue certain things together. The following part of the POM-file is for example defining which classes are searched for to execute tests (=scenarios):

   ...
          <execution>
            <id>run-stories-as-embeddables</id>
            <phase>integration-test</phase>
            <configuration>
              <includes>
                <include>**/*Scenarios.java</include>
              </includes>
              <ignoreFailureInStories>true</ignoreFailureInStories>
              <ignoreFailureInView>false</ignoreFailureInView>
           </configuration>
           <goals>
              <goal>run-stories-as-embeddables</goal>
           </goals>
         </execution>
   ...

... <execution> <id>run-stories-as-embeddables</id> <phase>integration-test</phase> <configuration> <includes> <include>**/*Scenarios.java</include> </includes> <ignoreFailureInStories>true</ignoreFailureInStories> <ignoreFailureInView>false</ignoreFailureInView> </configuration> <goals> <goal>run-stories-as-embeddables</goal> </goals> </execution> ...

Based on the name of the “scenario class” – in this case StackScenarios.java – the corresponding story-file can be found. Therefore the story-file must have a name that matches the name of the “scenario class”, unless it is allowed to add additional underscores and the mapping is not case-sensitive. In our example the name of the story-file is stack_scenarios.story.

JBehave offers different possibilities to search for story-files. In our example an absolute path is used for this, which contains the current working directory. Therefore the tests must be executed from the project directory containing the POM-file. The name of the story-file is in addition prepended by the path that is mathcing the Java package of the scenario class. This must be considered when storing the story-files.

The scenario class contains a method that configures the class (or classes) that contain the required step definitions (methods):

...
    @Override
    public List<CandidateSteps> candidateSteps() {
        return new InstanceStepsFactory(configuration(), new StackSteps())
                .createCandidateSteps();
    }
...

... @Override public List<CandidateSteps> candidateSteps() { return new InstanceStepsFactory(configuration(), new StackSteps()) .createCandidateSteps(); } ...

The corresponding classes must be reachable via the classpath, but probably it would be quite hard to make the implementation in a way that those classes can not be found.

Conclusion

Due to the length of this article one might think that JBehave is not as lightweight as promised in the introduction. But this is not true as the example in this article is covering for sure 80%-90% of the required implementation to get started with writing Acceptance Tests in a real project. The effort for this of course highly depends on the complexity of the tested software/system. But this is true for every tool used. An advantage here is that Java developers can probably estimate quite easily what is supported already by existing libraries and how much effort is thus required for writing the final test code.

The structure of the story-files is simple and clean. Furthermore there are additional features like the usage of tables with test-data to iterate over some scenarios with different test values. It is also possible to use some meta statements in the story-files to influence the reporting if this is desired. The reporting looks sufficient on first sight and can be extended still according to the documentation.

The good integration with Maven and Eclipse is the big strength of JBehave and makes it especially interesting for Java developers. The possibility to execute the tests easily using JUnit (e. g. for debugging purposes) is an additional advantage here.

Integrating JBehave into a CI-environment (Continuous Integration) is easily possible due to the usage of Maven. Testing Web-Applications is in addition supported with an own web component that is based on Selenium and of course one has the freedom to use the Java selenium library right away.

My very personal conclusion: Getting started with JBehave is fun and is having only very few moments of frustration (mainly when reading the API documentation). And even though I am not the biggest Maven-fan on this planet I have to admit that the integration with Maven is well done and helpful. I could well consider using the tool in some real-life project. Of course there are also still some more things to explore. For me most interesting here would be support of different languages in the story-files and testing web applications. Hopefully some of these aspects can be checked out in some forthcoming blog article.

Download

Here all the files required for the example can be downloaded as one ZIP-file. Afterwards the project can for example be imported to Eclipse.

cc_JBehaveSample.zip

The project can then be build issuing the following command:

mvn compile

The required style-files for the reporting can be downloaded using this command (required only once):

mvn generate-resources

Afterwards the tests can be executed using this command:

mvn integration-test

All commands must be executed from the project directory that is also containing the pom.xml-file.