A Guide to Behavior-Driven Development (BDD) with JBehave

7 minute read

Behavior-Driven Development (BDD) is a software development methodology that evolved from Test-Driven Development (TDD). Its primary goal is to bridge the communication gap between technical teams (developers, QA) and non-technical stakeholders (business analysts, product owners) by using a common, natural language to describe a system’s behavior.

TDD vs. BDD

While related, TDD and BDD have different focuses:

  • TDD (Test-Driven Development): Focuses on the implementation of a component from a developer’s perspective. The cycle is “Red-Green-Refactor,” where you write a failing test for a small unit of functionality, write the code to make it pass, and then refactor. The tests are often written in code and describe how a unit works.

  • BDD (Behavior-Driven Development): Focuses on the behavior of the system from a user’s perspective. It starts with a conversation about a feature, which is then captured in a structured, natural language format. BDD tests verify that the system behaves as expected by the business. It’s an “outside-in” approach.

In short, BDD is an extension of TDD that emphasizes collaboration and business value.

Fundamental Testing Types

Before diving deeper into BDD, it’s helpful to understand where it fits within the broader landscape of software testing. Here are some fundamental testing types and their ideal use cases.

Component Testing

  • What it is: Component testing involves testing individual components or modules of an application in isolation. It’s a step up from unit testing (which tests a single class) but stops short of full end-to-end testing. A component could be a group of related classes, like a service and its mocked dependencies.
  • Ideal Candidates:
    • A specific service and its interface (e.g., a UserService with its repositories mocked).
    • A UI component and its local state management (e.g., a React login form).
    • A data access layer module, testing its interaction with a mocked or in-memory database.
    • Any part of the system with a clear boundary that can be tested independently.

Functional Testing

  • What it is: Functional testing verifies that the software meets the specified functional requirements. It’s a form of black-box testing where the internal logic is not considered; the focus is on inputs and their corresponding outputs. BDD is a powerful way to structure and write functional tests.
  • Ideal Candidates:
    • User authentication flows (login, logout, password reset).
    • Core business workflows (e.g., adding an item to a shopping cart, completing a checkout process).
    • API endpoints: testing that a POST request to /users correctly creates a user and returns a 201 Created status.
    • Any feature with a clearly defined requirement from the business or product perspective.

Regression Testing

  • What it is: Regression testing is the process of re-running existing tests to ensure that new code changes, bug fixes, or refactoring have not broken or negatively impacted existing functionality. It acts as a safety net against unintended side effects.
  • Ideal Candidates:
    • The entire automated test suite: Ideally, all unit, component, and functional tests should be run as part of the regression suite.
    • High-traffic user paths: Core workflows that are critical to the business (e.g., checkout, search, payment processing).
    • Areas with recent major changes: Tests covering modules that were recently modified or refactored.
    • Tests for past bug fixes: Specific tests that were written to reproduce and then verify a bug fix, ensuring the bug does not reappear.

Smoke Testing

  • What it is: Smoke testing (also known as build verification testing) is a preliminary set of tests run on a new build to ensure its most critical functionalities are working. If a smoke test fails, the build is rejected, saving the time and resources of running more extensive tests. It answers the question: “Is the build stable enough to be tested further?”
  • Ideal Candidates:
    • Application startup.
    • User login and logout.
    • Access to the main page or dashboard of the application.
    • A single, simple end-to-end transaction (e.g., searching for a product).
    • Health check endpoints of microservices.

Manual Testing

  • What it is: Manual testing is the process where a human tester interacts with the application to find defects without the use of automation scripts. It leverages human intelligence, curiosity, and experience to find issues that automated tests might miss.
  • Ideal Candidates:
    • Exploratory Testing: Unscripted, free-form testing where the tester explores the application to discover unexpected behavior.
    • Usability Testing: Assessing how user-friendly and intuitive the application is. This requires human observation and feedback.
    • Complex, hard-to-automate scenarios: Workflows that are extremely complex, change frequently, or require interaction with external physical devices.
    • Visual validation: Checking for UI/UX issues, such as layout, color, and element alignment, which can be difficult to automate reliably.

The Core of BDD: Given-When-Then

BDD uses a structured syntax known as Gherkin to define acceptance criteria for a feature. This makes the requirements readable by everyone.

  • Given: Describes the initial context or prerequisite state of the system. (e.g., Given the user is logged in as an administrator)
  • When: Describes an action or event that occurs. (e.g., When the user clicks the 'Delete User' button)
  • Then: Describes the expected outcome or result. (e.g., Then the user should be removed from the system)

Introduction to JBehave

JBehave is a popular open-source framework for BDD in Java. It allows you to write executable specifications in plain text (called “stories”) and connect them to your Java code.

JBehave Core Concepts

  • Story: A plain text file (usually with a .story extension) that describes a feature. It contains one or more scenarios.
  • Scenario: A single, concrete example of a behavior within a story. Each scenario consists of a series of steps.
  • Steps: The Given, When, and Then parts of a scenario. Each step maps to a method in a Java class.

Getting Started: A JBehave Example

Let’s create a simple JBehave test for a calculator.

Step 1: Set up the Project (Maven)

You need to add the jbehave-core dependency to your pom.xml.

<properties>
    <jbehave.version>4.8.3</jbehave.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.jbehave</groupId>
        <artifactId>jbehave-core</artifactId>
        <version>${jbehave.version}</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Step 2: Write the Story

Create a file named calculator.story in src/test/resources/stories/.

Story: A user wants to perform calculations

Scenario: Adding two numbers
Given a calculator
When I add 5 and 7
Then the result should be 12

Step 3: Implement the Step Definitions

Create a Java class to implement the steps defined in the story. JBehave uses annotations (@Given, @When, @Then) to map these steps to methods.

src/test/java/com/example/CalculatorSteps.java:

package com.example;

import org.jbehave.core.annotations.Given;
import org.jbehave.core.annotations.Then;
import org.jbehave.core.annotations.When;
import static org.junit.Assert.assertEquals;

public class CalculatorSteps {

    private Calculator calculator;
    private int result;

    // A simple class for our example
    class Calculator {
        public int add(int a, int b) {
            return a + b;
        }
    }

    @Given("a calculator")
    public void givenACalculator() {
        calculator = new Calculator();
    }

    @When("I add $a and $b")
    public void whenIAddTwoNumbers(int a, int b) {
        result = calculator.add(a, b);
    }

    @Then("the result should be $expected")
    public void thenTheResultShouldBe(int expected) {
        assertEquals(expected, result);
    }
}

Notice how JBehave automatically parses the numbers from the steps and passes them as parameters to the methods.

Step 4: Configure and Run the Story

Finally, create a runner class. This class configures JBehave and specifies which stories to run. It can be run as a standard JUnit test.

src/test/java/com/example/CalculatorStoryRunner.java:

package com.example;

import org.jbehave.core.configuration.Configuration;
import org.jbehave.core.configuration.MostUsefulConfiguration;
import org.jbehave.core.io.LoadFromClasspath;
import org.jbehave.core.junit.JUnitStory;
import org.jbehave.core.reporters.Format;
import org.jbehave.core.reporters.StoryReporterBuilder;
import org.jbehave.core.steps.InjectableStepsFactory;
import org.jbehave.core.steps.InstanceStepsFactory;

public class CalculatorStoryRunner extends JUnitStory {

    @Override
    public Configuration configuration() {
        return new MostUsefulConfiguration()
            // Where to find story files
            .useStoryLoader(new LoadFromClasspath(this.getClass()))
            // How to report test execution
            .useStoryReporterBuilder(new StoryReporterBuilder()
                .withDefaultFormats()
                .withFormats(Format.CONSOLE, Format.TXT));
    }

    @Override
    public InjectableStepsFactory stepsFactory() {
        // The class(es) containing step definitions
        return new InstanceStepsFactory(configuration(), new CalculatorSteps());
    }
}

When you run CalculatorStoryRunner with JUnit, JBehave will:

  1. Read calculator.story.
  2. Execute each step (Given, When, Then).
  3. Call the corresponding Java method from CalculatorSteps.
  4. Report the results to the console.

JBehave vs. Cucumber

Cucumber is another very popular BDD framework. Both are excellent choices, but they have some differences:

Feature JBehave Cucumber
Origin One of the original Java BDD frameworks. Originated in the Ruby world, now has a strong Java implementation (Cucumber-JVM).
Configuration Configuration is often done in a Java runner class. Tends to use a mix of annotations and a properties file.
Flexibility Considered very flexible and configurable, but can require more boilerplate code. Often seen as slightly easier to get started with due to more conventions.
Community Solid, mature community. Larger, more active community with extensive documentation and third-party support.

Ultimately, the choice between them often comes down to team preference.

BDD Benefits & Challenges

Benefits

  • Clarity & Collaboration: Creates a shared understanding of features among all team members.
  • Focus on Business Value: Ensures development is focused on meeting real user needs.
  • High Visibility: The plain-text scenarios make it easy for anyone to see what the system does and how it’s tested.
  • Favors Automation: BDD tests are automated from the start.

Challenges

  • Strong Collaboration Required: BDD fails if teams work in silos. It requires active engagement from business, dev, and QA.
  • Writing Good Scenarios: It takes practice to write scenarios that are clear, concise, and focused on behavior rather than UI details.
  • Maintenance: As the system grows, maintaining a large suite of BDD tests can be challenging.