JBehave is a Java testing framework for Behavior-Driven Development (BDD.) It’s been around for many years with regular updates, including an update to 5.0 in 2022. But, it still doesn’t get the attention and respect it deserves.
In this article, we’ll take a quick look at what BDD is and how you can use it to up your testing game. Then, we’ll set up a few simple tests with JBehave so you can see how easy it is to add to an application.
What’s BDD?
BDD is a variation of Test-Driven Development that aims to use the language of end-users to describe tests rather than code.
In TDD, you write test code, write application code that passes the test, refine the code, etc. In BDD, you start with scenarios or stories similar to Agile user stories. So, you write tests that verify the behavior defined in the product specification.
Like TDD, BDD doesn’t have formal definitions for what a test should look like, but there is a recommended framework or shape.
Here’s the test we’re going to write with JBehave:
Scenario: I have a Thing and I can add items to it Given I have a Thing with an item named foo When I add an item named bar Then My thing has 2 items And My thing has an item named bar
It’s written in plain English, clearly explaining how the test starts, the steps performed, and what it looks like when it’s finished.
Let’s look at JBehave and get to work!
Expand Your Test Coverage
What Is JBehave?
JBehave is a Java framework for writing and running BDD tests. It has many moving parts that make it easy to fit into your workflow and tools.
- You can write your tests with the DSL in the example above or Gherkin syntax.
- JBehave tests are easy to run with Eclipse, IntelliJ, Ant, Maven, and Junit.
- You can compose your test steps with many tools, including Groovy and Selenium.
We’re going to keep it simple with this tutorial and focus on writing tests in the DSL and composing our steps in Java. We’ll use Maven to build the code.
Getting Started With JBehave
Create a JBehave Maven Project
You can find the code for this tutorial here, but I recommend you go through these steps.
JBehave supplies us with a Maven Archetype that makes starting a new project easy. So, let’s start there.
$ mvn archetype:generate -Dfilter=org.jbehave:jbehave
First, the archetype asks what kind of project you want to use:
Choose archetype: 1: remote -> org.jbehave:jbehave-groovy-archetype (An archetype to run multiple textual stories with steps classes written in Groovy.) 2: remote -> org.jbehave:jbehave-guice-archetype (An archetype to run multiple textual stories configured programmatically but with steps classes composed using Guice.) 3: remote -> org.jbehave:jbehave-needle-archetype (An archetype to run multiple textual stories configured programmatically but with steps classes composed using Needle.) 4: remote -> org.jbehave:jbehave-pico-archetype (An archetype to run multiple textual stories configured programmatically but with steps classes composed using Pico.) 5: remote -> org.jbehave:jbehave-simple-archetype (An archetype to run multiple textual stories configured programmatically.) 6: remote -> org.jbehave:jbehave-spring-archetype (An archetype to run multiple textual stories configured programmatically but with steps classes composed using Spring.) 7: remote -> org.jbehave.web:jbehave-web-selenium-flash-archetype (An archetype to run web Flash stories using Selenium.) 8: remote -> org.jbehave.web:jbehave-web-selenium-groovy-pico-archetype (An archetype to run web stories using Selenium, Groovy and Pico.) 9: remote -> org.jbehave.web:jbehave-web-selenium-java-spring-archetype (An archetype to run web stories using Selenium, Java and Spring.) Choose a number or apply filter (format: [groupId:]artifactId, case sensitive contains): : 5
Here you see the different integrations JBehave supports. There are nine different options! We’ll use #5 for this project since it’s the most straightforward.
Next, JBehave asks which version you want to use.
82: 4.3 83: 4.3.1 84: 4.3.2 85: 4.3.3 86: 4.3.4 87: 4.3.5 88: 4.4 89: 4.5 90: 4.5.1 91: 4.6 92: 4.6.1 93: 4.6.2 94: 4.6.3 95: 4.7 96: 4.8 97: 4.8.1 98: 4.8.2 99: 4.8.3 100: 5.0-SNAPSHOT Choose a number: 100:
When I wrote this, there were 100 different options. I went with the latest. Even though it’s a snapshot release, version 5.0 changes the interface enough that we should use the new interfaces.
[INFO] Using property: jbehaveCoreVersion = 5.0-SNAPSHOT [INFO] Using property: jbehaveSiteVersion = 3.4.1 Define value for property 'groupId': com.ericgoebelbecker Define value for property 'artifactId': jbehave Define value for property 'version' 1.0-SNAPSHOT: : Define value for property 'package' com.ericgoebelbecker: : Confirm properties configuration: jbehaveCoreVersion: 5.0-SNAPSHOT jbehaveSiteVersion: 3.4.1 groupId: com.ericgoebelbecker artifactId: jbehave version: 1.0-SNAPSHOT package: com.ericgoebelbecker Y: : [INFO] ---------------------------------------------------------------------------- [INFO] Using following parameters for creating project from Archetype: jbehave-simple-archetype:5.0-SNAPSHOT [INFO] ---------------------------------------------------------------------------- [INFO] Parameter: groupId, Value: com.ericgoebelbecker [INFO] Parameter: artifactId, Value: jbehave [INFO] Parameter: version, Value: 1.0-SNAPSHOT [INFO] Parameter: package, Value: com.ericgoebelbecker [INFO] Parameter: packageInPathFormat, Value: com/ericgoebelbecker [INFO] Parameter: jbehaveSiteVersion, Value: 3.4.1 [INFO] Parameter: package, Value: com.ericgoebelbecker [INFO] Parameter: jbehaveCoreVersion, Value: 5.0-SNAPSHOT [INFO] Parameter: groupId, Value: com.ericgoebelbecker [INFO] Parameter: artifactId, Value: jbehave [INFO] Parameter: version, Value: 1.0-SNAPSHOT [WARNING] Don't override file /home/egoebelbecker/PycharmProjects/jbehave/src/main/java/com/ericgoebelbecker [WARNING] Don't override file /home/egoebelbecker/PycharmProjects/jbehave/src/main/resources/com/ericgoebelbecker [INFO] Project created from Archetype in dir: /home/egoebelbecker/PycharmProjects/jbehave [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 23.904 s [INFO] Finished at: 2022-03-24T22:01:47-04:00 [INFO] ------------------------------------------------------------------------
Next, it will prompt you for a groupId, artifactId, and version. Then it will build your skeleton project for you. Maven will put it in a subdirectory named for your artifactId.
Verify Your Pom File
When I ran the archetype, it omitted two critical dependencies from the project. So, check your pom.xml dependencies section.
Make sure guava and hamcrest are in the <dependencies> section. The archetype may have been fixed, so check before you add them.
<dependency> <groupId>org.hamcrest</groupId> <artifactId>hamcrest</artifactId> <version>2.2</version> <scope>test</scope> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>31.0.1-jre</version> <optional>true</optional> </dependency>
Update Some Code
We need to make one more change related to version 5.0 and some name and argument changes. The archetype still has the old code.
Open src/main/java/<package>/MyStories.java.
First, find this import:
import org.jbehave.core.junit.JUnitReportingRunner;
Change it to this:
import org.jbehave.core.junit.JUnit4StoryRunner;
Then, find this line and delete it:
import org.jbehave.core.reporters.CrossReference;
Next, find this line. It’s right above the class definition:
@RunWith(JUnit4StoryRunner.class)
Change it to reflect the new class name:
@RunWith(JUnitReportingRunner.class)
A few lines down, you’ll see this:
public MyStories() { configuredEmbedder().embedderControls().doGenerateViewAfterStories(true).doIgnoreFailureInStories(true) .doIgnoreFailureInView(true).useThreads(2).useStoryTimeoutInSecs(60); }
Remove the call to useStoryTimeoutInSecs():
public MyStories() { configuredEmbedder().embedderControls().doGenerateViewAfterStories(true).doIgnoreFailureInStories(true) .doIgnoreFailureInView(true).useThreads(2); }
Finally, find this line:
ExamplesTableFactory examplesTableFactory = new ExamplesTableFactory(new LocalizedKeywords(), resourceLoader, parameterConverters, parameterControls, tableTransformers);
And change the call to look like this:
ExamplesTableFactory examplesTableFactory = new ExamplesTableFactory(resourceLoader,tableTransformers);
This class loads your stories and steps and makes them available to Maven (or your IDE) for running as Junit tests.
To wrap it up, you can rename the class to suit your project if you wish.
Write Your Stories
Now, let’s write some code.
Inside your project, you’ll find a file named my.story in the src/main/resources/stories/ directory:
Scenario: A scenario with some pending steps Given I am a pending step And I am still pending step When a good soul will implement me Then I shall be happy
This directory is where Maven will look for your user stories. The archetype includes this file with a fun sample.
Rename (or replace) the file with addItemsToThings.story and add our sample story:
Scenario: I have a Thing and I can add items to it Given I have a Thing with an item named foo When I add an item named bar Then My thing has 2 items And My thing has an item named bar
If you try to run the tests now, they’ll fail because you haven’t implemented the code.
$ mvn integration-test [INFO] Scanning for projects... [INFO] [INFO] ------------------------------< foo:foo >------------------------------- [INFO] Building JBehave Stories 1.0-SNAPSHOT [INFO] --------------------------------[ jar ]--------------------------------- [INFO] (trimmed for space) [INFO] Running story foo/stories/my.story (foo/stories/my.story) BeforeSystemStorySteps BeforeUserStorySteps Scenario: I have a Thing and I can add items to it BeforeSystemScenarioSteps BeforeUserScenarioSteps Given I have a Thing with an item named foo (PENDING) When I add an item named bar (PENDING) Then My thing has 2 items (PENDING) Then My thing has an item named bar (PENDING) @Given("I have a Thing with an item named foo") @Pending public void givenIHaveAThingWithAnItemNamedFoo() { // PENDING } @When("I add an item named bar") @Pending public void whenIAddAnItemNamedBar() { // PENDING } @Then("My thing has 2 items") @Pending public void thenMyThingHas2Items() { // PENDING } @Then("My thing has an item named bar") @Pending public void thenMyThingHasAnItemNamedBar() { // PENDING } AfterUserScenarioSteps (trimmed) [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 2.280 s [INFO] Finished at: 2022-03-25T14:47:52-04:00 [INFO] ------------------------------------------------------------------------
JBehave tells us precisely what we need to do: implement our steps code.
Let’s do it!
Write Your Steps
The archetype put a mySteps.java file in the steps subdirectory of your application code. Right now, it’s an unimplemented class:
public class MySteps { }
The output from the first run gives us an idea of what JBehave is looking for: methods with names like givenIHaveAThingWithAnItemNamedFoo and thenMyThingHasAnItemNamedBar.
But, naming the methods isn’t what makes them work with JBehave. It’s a set of annotations: @Given, @When, and @Then.
Here’s a working version, renamed to ThingSteps:
public class ThingSteps { private final ThreadLocal<Thing> thingStash = new ThreadLocal<>(); private Thing getThing() { return this.thingStash.get(); } @Given("I have a Thing with an item named $name") public void givenIHaveAThingWithAnItemNamed(String name) { Thing thing = new Thing(name); thingStash.set(thing); } @When("I add an item named $name") public void whenIAddAnItemNamed(String name) { getThing().addThing(name); } @Then("My thing has $count items") public void thenThingHasName(int count) { Assert.assertEquals(getThing().getThingCount(), count); } @Then("My thing has an item named $name") public void thenThingHasName(String name) { Assert.assertTrue(getThing().hasThing(name)); } }
On line #3, we create ThreadLocal storage to store our Thing between test steps, and on line #5, we have a method for grabbing the current instance.
Line #9 starts our first step. The @Given annotation tells JBehave it’s a Given step. JBehave matches the argument to the annotation to the text in the story.
Let’s walk through the first line of our story:
Given I have a Thing with an item named foo
This matches line #9, givenIHaveAThingWithAnItemNamed:
@Given("I have a Thing with an item named $name") public void givenIHaveAThingWithAnItemNamed(String name) { Thing thing = new Thing(name); thingStash.set(thing); }
The expression passed to @Given has a variable in it: $name. So JBehave passes “foo” to the method.
If you look at the rest of the steps, you can see how they’re run. But we’re missing something. Where’s the code under test? We need a Thing to test!
Write Your Code
Create a new subdirectory in your codebase named model.
Here’s the model code:
public class Thing { private ArrayList<String> theThings = new ArrayList<>(); public Thing() { } public Thing(String name) { theThings.add(name); } public void addThing(String name) { theThings.add(name); } public int getThingCount() { return theThings.size(); } public boolean hasThing(String name) { return theThings.contains(name); } }
Nothing complicated here. Just a wrapper for an ArrayList of Strings.
Run Your Tests
Let’s give it a spin.
Here’s the complete output from Maven:
$ mvn integration-test [INFO] Scanning for projects... [INFO] [INFO] --------------------< com.ericgoebelbecker:jbehave >-------------------- [INFO] Building JBehave Stories 1.0-SNAPSHOT [INFO] --------------------------------[ jar ]--------------------------------- [INFO] [INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ jbehave --- [WARNING] File encoding has not been set, using platform encoding UTF-8, i.e. build is platform dependent! [WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent! [INFO] Copying 0 resource [INFO] Copying 1 resource [INFO] [INFO] --- jbehave-maven-plugin:5.0-SNAPSHOT:unpack-view-resources (unpack-view-resources) @ jbehave --- [INFO] Unpacked /home/egoebelbecker/.m2/repository/org/jbehave/jbehave-core/5.0-SNAPSHOT/jbehave-core-5.0-SNAPSHOT-resources.zip to /home/egoebelbecker/PycharmProjects/jbehave/target/jbehave/view [INFO] Unpacked /home/egoebelbecker/.m2/repository/org/jbehave/site/jbehave-site-resources/3.4.1/jbehave-site-resources-3.4.1.zip to /home/egoebelbecker/PycharmProjects/jbehave/target/jbehave/view [INFO] [INFO] --- maven-compiler-plugin:2.1:compile (default-compile) @ jbehave --- [INFO] Nothing to compile - all classes are up to date [INFO] [INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ jbehave --- [WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent! [INFO] skip non existing resourceDirectory /home/egoebelbecker/PycharmProjects/jbehave/src/test/resources [INFO] [INFO] --- maven-compiler-plugin:2.1:testCompile (default-testCompile) @ jbehave --- [INFO] No sources to compile [INFO] [INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ jbehave --- [INFO] No tests to run. [INFO] [INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ jbehave --- [INFO] Building jar: /home/egoebelbecker/PycharmProjects/jbehave/target/jbehave-1.0-SNAPSHOT.jar [INFO] [INFO] --- jbehave-maven-plugin:5.0-SNAPSHOT:run-stories-as-embeddables (embeddable-stories) @ jbehave --- [INFO] Running stories as embeddables using embedder Embedder[classLoader=EmbedderClassLoader[urls=[/home/egoebelbecker/PycharmProjects/jbehave/target/classes/, jbehave-core-5.0-SNAPSHOT.jar, junit-platform-launcher-1.8.2.jar, junit-platform-engine-1.8.2.jar, opentest4j-1.2.0.jar, junit-platform-commons-1.8.2.jar, apiguardian-api-1.1.2.jar, junit-jupiter-5.8.2.jar, junit-jupiter-api-5.8.2.jar, junit-jupiter-params-5.8.2.jar, junit-vintage-engine-5.8.2.jar, junit-4.13.2.jar, hamcrest-core-1.3.jar, commons-io-2.11.0.jar, commons-lang3-3.12.0.jar, commons-text-1.9.jar, plexus-utils-3.4.1.jar, freemarker-2.3.31.jar, gson-2.9.0.jar, paranamer-2.8.jar, guava-31.0.1-jre.jar, failureaccess-1.0.1.jar, listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar, jsr305-3.0.2.jar, checker-qual-3.12.0.jar, error_prone_annotations-2.7.1.jar, j2objc-annotations-1.3.jar],parent=ClassRealm[plugin>org.jbehave:jbehave-maven-plugin:5.0-SNAPSHOT, parent: jdk.internal.loader.ClassLoaders$AppClassLoader@277050dc]],configuration=org.jbehave.core.configuration.MostUsefulConfiguration@78d9f51b,embedderControls=UnmodifiableEmbedderControls[EmbedderControls[batch=false,failOnStoryTimeout=false,generateViewAfterStories=true,ignoreFailureInStories=true,ignoreFailureInView=false,skip=false,storyTimeouts=300,threads=1,verboseFailures=false,verboseFiltering=false]],embedderFailureStrategy=<null>,embedderMonitor=MavenEmbedderMonitor,executorService=<null>,executorServiceCreated=false,metaFilters=[],metaMatchers=<null>,performableTree=PerformableTree,stepsFactory=<null>,storyManager=<null>,storyMapper=StoryMapper,systemProperties=<null>,timeoutParsers=<null>] [INFO] Found class names: [com.ericgoebelbecker.ThingStories] [INFO] Using controls UnmodifiableEmbedderControls[EmbedderControls[batch=false,failOnStoryTimeout=false,generateViewAfterStories=true,ignoreFailureInStories=true,ignoreFailureInView=false,skip=false,storyTimeouts=300,threads=1,verboseFailures=false,verboseFiltering=false]] [INFO] Running embeddable com.ericgoebelbecker.ThingStories [INFO] Processing system properties {} [INFO] Using controls UnmodifiableEmbedderControls[EmbedderControls[batch=false,failOnStoryTimeout=false,generateViewAfterStories=true,ignoreFailureInStories=true,ignoreFailureInView=false,skip=false,storyTimeouts=300,threads=1,verboseFailures=false,verboseFiltering=false]] BeforeStories [INFO] Running story com/ericgoebelbecker/stories/addItemsToThings.story (com/ericgoebelbecker/stories/addItemsToThings.story) BeforeSystemStorySteps BeforeUserStorySteps Scenario: I have a Thing and I can add items to it BeforeSystemScenarioSteps BeforeUserScenarioSteps Given I have a Thing with an item named foo When I add an item named bar Then My thing has 2 items Then My thing has an item named bar AfterUserScenarioSteps AfterSystemScenarioSteps AfterUserStorySteps AfterSystemStorySteps AfterStories [INFO] Generating reports view to '/home/egoebelbecker/PycharmProjects/jbehave/target/jbehave' using formats '[stats, console, txt, html, xml]' and view properties '{reports=ftl/jbehave-reports.ftl, nonDecorated=ftl/jbehave-report-non-decorated.ftl, maps=ftl/jbehave-maps.ftl, views=ftl/jbehave-views.ftl, decorated=ftl/jbehave-report-decorated.ftl}' [INFO] Reports view generated with 3 stories (of which 0 pending) containing 2 scenarios (of which 0 pending) [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 1.867 s [INFO] Finished at: 2022-03-25T16:12:25-04:00 [INFO] ------------------------------------------------------------------------ $
Success! JBehave echoed our test steps, and the build succeeded.
This is only the tip of the iceberg. You can write your own DSL with JBehave, and modify your test grammar with regular expressions. You can also integrate your tests with frameworks like Spring and Guava.
Add BDD to Your Tests
In this tutorial, we briefly discussed BDD and JBehave. Then we built a simple project from scratch. We tested a straightforward Java POJO with a short user story. As this tutorial demonstrated, JBehave and BDD make your tests easy to understand and allow you to test against user stories written in English.
Now that you know how to get started, there’s no reason not to add BDD to your Java tests today!