January 30, 2023

Testing With Behavior Driven Development

Jason Spangler

Developers often use unit tests to determine code coverage. Many times, unit testing is abused to perform integration testing rather than to verify a single functional unit (a function with no side effects) works as expected.

Adopting Behavior Driven Development testing patterns verifies that an application feature reliably reproduces expected business behaviors.

What is Behavior Driven Development?

Behavior Driven Development (BDD) is an agile process which describes the desired functionality of an application as a set of behaviors.

A behavior is how the system or application responds to a given stimulus or input.

BDD describes behaviors through specifications which include Narrative and Acceptance Criteria.

Narrative

The Narrative is a short story that is a common part of defining the Stories used in Agile/Scrum to relate between developers and stake holders what is being built.

The Narrative typically follows the structure:

  • As a person or role
  • I want to do or have thing
  • So that benefit or value achieved

Acceptance Criteria

The Acceptance Criteria for a Behavior is Scenario driven. Each Scenario describes a preset condition and action along with the expected outcome.

The Acceptance Criteria follows the structure:

  • Scenario title describing the overall conditions and expected outcome
  • Given the initial conditions of the situation
  • When an action is taken, or event occurs
  • Then an expected outcome occurs

Why use BDD in testing?

Aligning the test scenarios with the business goals of the application through a common Domain Specific Language (DSL) clarifies to all stakeholders what that software is achieving.

Keeping the goals of the software in a common DSL understood by both the software and business leadership teams creates more accurate communication. This communication allows all stakeholders to determine whether the application under development is achieving the required business goals.

Consider the following test cases first described as unit tests and then described as a Scenario:

Unit Test Example

private void doesCreateApplicantFromSubmission() {...}
private void isApplicantValid() {...}
private void doesUpdateApplicantToCustomer() {...}

In the above unit tests, we are checking the path of storing an Applicant data object, determining if the Applicant object evaluates correctly as valid (where valid may mean several things), and if the Applicant is promoted to a Customer data object as three (3) separate disjointed tests.

BDD Test Example

Scenario: Registering a Safe and Valid Applicant as a Customer

Given a person submits a customer application

When the application is reviewed

And the applicant is confirmed as safe and valid

Then the application is registered as a customer in the system

In the above scenario, we are describing the process of a person moving from potential customer to customer.

The question that quickly arises from the example: where’s the code?

BDD testing tools: Cucumber

There are many BDD testing tools which focus on transforming the Scenario script into executable code.

One of the more popular available cross-platform frameworks is Cucumber.

The following demonstrates using Cucumber with Java and Spring Boot.

Project Dependencies

In a Spring Boot project, include the Cucumber BOM within your managed dependencies.

The following example assumes maven as the built tool:

<dependencyManagement>
  <dependencies>
    ...
    <dependency>
      <groupId>io.cucumber</groupId>
      <artifactId>cucumber-bom</artifactId>
      <version>${cucumber-bom.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
    ...
  </dependencies>
</dependencyManagement>
...


With the BOM in-place, we can add the following test scope dependencies:

...
<dependency>
  <groupId>io.cucumber</groupId>
  <artifactId>cucumber-gherkin</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>io.cucumber</groupId>
  <artifactId>cucumber-java</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>io.cucumber</groupId>
  <artifactId>cucumber-spring</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>io.cucumber</groupId>
  <artifactId>cucumber-junit-platform-engine</artifactId>
  <scope>test</scope>
</dependency>
...

Configuration

To execute Cucumber feature files, we need to define:

  • The directory under resources which contains the *.feature files describing the Scenarios
  • The directory which contains the *Steps.java files to execute
  • A configuration file which provides the “glue” tying feature files to executable code

Feature File Directory

A feature file describes the scenarios associated with a given feature of the application or system.

In the test/resources directory of the spring boot project, create a directory named “features” which will contain our feature file tests.

Step File Directory

Each Scenario is a collection of Steps which follow the given, when, then format. The code associated with these steps is placed in a “steps” directory by convention.

In the src/main/test/java/{com.your.package} directory, create a new directory named “steps”. This will contain the “*Steps.java” files referenced at runtime by Cucumber to perform the steps of the feature Scenarios.

Configuration Files

The Configuration for Cucumber will use two files: a TestConfig.java and a CucumberConfig.java file.

@Configuration
@ContextConfiguration( classes = {...})
@TestPropertySource("classpath:application.properties")
public class TestConfig {

}

The TestConfig.java is the normal Spring Boot test config file used for Junit testing:

Note that this is a standard configuration with the following annotations:

  • Configuration – indicates that the file is meant to configure the module
  • ContextConfiguration – points to config classes in the module under test if the module does not contain a ‘main’ class.
  • TestPropertySource – points to application.properties file in the src/main/test/resources  directory which performs any special application configuration for test purposes only.  

The CucumberConfig.java is used to configure the glue between the *.feature files and the *Steps.java files.

@Suite
@CucumberContextConfiguration
@IncludeEngines("cucumber")
@SelectClasspathResource("features")
public class CucumberConfig extends TestConfig{

}

 

Note that the Cucumber configuration extends the TestConfig class to include it with the test configuration.  

The annotations are as follows:  

  • Suite – marks this configuration as using the Junit test runner.
  • CucumberContextConfiguration – marks this configuration as a Cucumber context configuration.
  • IncludeEngines – include the Cucumber engine during testing.
  • SelectClasspathResources – annotation that specifies where under the resources directory to find the feature files.

That gives Junit enough information to allow testing the feature files.

Feature Definitions

Feature files define the Scenarios which describe how the feature (for which the file should be named) should behave under the given conditions.

In the “features” directory, a feature file named CustomerRegistry.feature may have a Scenario describing a successful customer registration process using a KYC service (a financial industries term for Know Your Customer).

Feature:

  Registering a customer into the application.

  Scenario: Register Applicant as Customer with Accepted KYC (Know Your Customer) Application
    
    Given an Applicant submits an application with valid KYC responses
    When the KYC service evaluates the responses
    Then the applicant is promoted to Customer
    And a customer checking account is created 

 The Scenario describes the following conditions:

  • Given a User applies to be a customer for the service using information that is valid for the Know Your Customer process …
  • When that KYC process evaluates the information (known to be valid) for the applicant …
  • Then the service will upgrade the applicant User to a customer User in the system …
  • And a customer checking account will be created

The Scenario creates a compound test of ensuring that a single condition (successful application with valid KYC information) produces a series of expected results (upgrading the User to a Customer and creating a checking account for the customer to use).

To evaluate these expected behaviors, we need to create a CustomerRegistrySteps.java file in the steps directory.

Steps Definition

Create a CustomerRegistrySteps.java class file in the src/main/test/java/{com.your.package}/steps directory of your project.

In this file we will include annotations for each step of the scenario:

public class CustomerRegistrySteps {
  private static final Logger log = 
      LoggerFactory.getLogger(CustomerRegistrySteps.class);

  @Autowired
  private CustomerOrchestrator customerOrchestrator;

  @Autowired
  private CustomerService customerService;

  @Autowired
  private AccountService accountService;

  private PersonAsCustomer request;
  private PersonAsCustomer customer;

  @Given("an Applicant submits an application with valid KYC responses")
  private void givenAnApplicantSubmitsAnApplicationWithValidKycResponses(){
    log.info("Potential customer submits their application");
    request = CustomerFeatureHelper.makeCustomerRegistrationRequest();
  }

  @When("the KYC service evaluates the responses")
  private void theKycServiceEvaluatesTheResponses(){
    log.info("Process the request information as part of the Orchestration which performs KYC");
    customer = customerOrchestrator.registerPersonAsCustomer(request);
  }

  @Then("the applicant is promoted to Customer")
  private void theApplicantWillBePromotedToCustomer(){
    
    assertThat(customer).isNotNull();
    
    // check that our ID is generated
    assertThat(customer.getCustomerId()).isNotNull();

    // additional assertions ...
  }

  @And ("a customer checking account is created")
  private void aCustomerCheckingAccountWillBeCreated(){
    // verify that we have a checking account
    AccountDetailsResponse accountDetails =
        AccountFeatureHelper.getAccountDetails(
            accountService,
            customerService,
            customer
        );

    // console log our response from our service
    log.info("Positions: {}", MessageConverters.toJson(accountDetails));

    String checkingPosnId = SweepFeatureHelper.getCheckingPosnId(accountDetails);

    assertThat(checkingPosnId).isNotNull();

    // additional assertions about the account created ...
  }
}

In the above class definition, we have done the following:

  • Added a @Given annotation to set the initial state of our system
  • Added a @When annotation to indicate that an action is performed on the system
  • Added a @Then annotation to verify the outcome of the action
  • Added a @And annotation to validate additional results as necessary

The file itself makes use of helper classes and services that are specific to the system under test. The importance here is demonstrating that custom code is used to simulate the states of the scenario at each step and execute those steps in code to get a result.

The result is then evaluated to determine if our system achieves the desired behaviors when the given states and conditions are applied.

A win-win for business and software development teams

Behavior Driven Development testing aligns business and developer goals by providing a common language in which to describe both success and failure conditions of an application.

Using a Given-When-Then approach to describing scenarios gives common context and understanding to what is expected of the software under development.

Describing behaviors and expected outcomes with a common understanding between business and development teams mitigates the risk of misunderstandings to delay or prevent feature development.

Contributors

Jason Spangler

Software Technical Lead
Alumni
Go to bio