Friday 17th May 2024
Ho Chi Minh, Vietnam

1. BDD introduction

BDD test stands for behavior-driven development test. It is a testing methodology used in agile software development and is an extension of Test-Driven Development (TDD). BDD test emphasizes developing features based on a user story and writing code that provides a behavior that matches the desired outcome of the feature written in a natural language.

The result is a set of functional specifications that outline executable scenarios for the software. BDD tests are designed to close the gap between business people and technical people by focusing on the behaviors that matter most to the business requirements.

A business requirement translated into a language such as Gherkin can be defined as:

Given Precondition

When Action

Then Result

These features/behaviors are mapped to test code that can execute and validate the behavior. In development, the practice is to define the scenario BEFORE writing any code. This ensures we’re building the right function that the business needs. There are many tools to develop BDD for example Cucumber, Parasoft, Recorder, Selenic, and SOAtest.

2. Cucumber overview

Cucumber is a tool that supports BDD with Gherkin language. With Cucumber, software behavior can be defined in plain, understandable language, and test cases can be written to match the desired behavior. Essentially, Cucumber provides a way to write tests that are easy to understand, and it serves as a supporting tool for automated testing in a BDD workflow.

Cucumber is a nontechnical language that’s easy to read and uses Given/When/Then statements to structure the behaviors of your software. Below is a sample Cucumber feature file about a login feature that is straightforward.

CucumberStudio is a collaborative testing platform in the cloud that allows the software delivery team (Product Owner, BA, QA, Developer, or whoever) to co-design Cucumber tests.

3. Spring Boot Cucumber Tests Example

We will create an Email Service application by Spring Boot which contains some REST APIs to perform some operations as below:

  • Send emails to contacts (or customers)
  • Track email open event
  • Get email send statistics (how many contacts were sent email successfully to, how many contacts opened email)

Then we will use Cucumber to create some BDD test cases to demonstrate how BDD helps us to develop software features that match with business requirements. You can check out this project over on Github

3.1. Prerequisites

  • Docker
  • Java 11
  • Gradle
  • Spring boot
  • Flyway

3.2. Database design

The database of this project is simple:

  • contact table to store the customer info, the unsubscribed column is a boolean that lets us know if a contact has already unsubscribed, so when sending an email to this contact, an ERROR status should be captured.
  • email table to store sent emails
  • email_contact is an association between email and contact to store which contacts an email was sent to which status column presents send email status which can be ERROR, SENT, or OPENED

3.3. Integrate docker-compose to BDD tests

To build up the entire application including the MySQL database via Docker compose we use gradle-docker-compose-plugin which supports composeUp and composeDown gradle tasks. Then we can easily add the composeUp task before running tests and the composeDown after running tests using dependsOn and finalizedBy. The approach is useful and enables us to handle the tedious setup especially when running BDD tests on a CI environment such as Jenkin.

build.gradle

plugins {
	id 'org.springframework.boot' version '2.3.0.RELEASE'
	id 'io.spring.dependency-management' version '1.0.9.RELEASE'
	id 'java'
	id 'io.freefair.lombok' version '5.0.0'
	id 'com.avast.gradle.docker-compose' version '0.16.12'
}

group = 'nht.demo-cucumber'
version = '0.0.1-SNAPSHOT'

java {
	sourceCompatibility = "11"
	targetCompatibility = "11"
}

repositories {
	mavenCentral()
}

ext {
	set('springCloudVersion', 'Hoxton.SR5')
}

dependencies {
	implementation 'org.flywaydb:flyway-core'
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'mysql:mysql-connector-java'
	implementation 'org.springframework.boot:spring-boot-starter-web'

	testImplementation 'io.cucumber:cucumber-java:7.13.0'
	testImplementation 'io.cucumber:cucumber-spring:7.13.0'
	testImplementation 'io.cucumber:cucumber-junit:7.13.0'
	testImplementation 'io.cucumber:cucumber-core:7.13.0'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

dockerCompose {
	useComposeFiles = ['docker/docker-compose.yml']
}

test {
	useJUnitPlatform()
	dependsOn composeUp
	finalizedBy composeDown
}

3.3. Implement RESTful APIs

We will create some REST APIs to perform some operations as mentioned before:

  • Send email to contacts: POST /emails/send
  • Track email opens: POST /emails/{emailId}/contacts/{contactId}/open
  • Get email send statistics: GET /emails/{emailId}/report

EmailController.java

@RestController
@RequiredArgsConstructor
@Slf4j
public class EmailController extends BaseController {

  private final EmailService emailService;


  @PostMapping("/emails/send")
  public ResponseEntity sendEmail(@RequestBody SendEmailDto sendEmailDto) {
    try {
      emailService.sendEmail(sendEmailDto);
      return ResponseEntity.ok().build();
    }
    catch (Exception ex) {
      return handleInternalServerError(ex);
    }
  }

  @PostMapping("/emails/{emailId}/contacts/{contactId}/open")
  public ResponseEntity openEmail(@PathVariable Long emailId, @PathVariable  Long contactId) {
    try {
      emailService.openEmail(emailId,contactId);
      return ResponseEntity.ok().build();
    }
    catch (Exception ex) {
      return handleInternalServerError(ex);
    }
  }

  @GetMapping("/emails/{emailId}/report")
  public ResponseEntity getEmailReport(@PathVariable Long emailId) {
    try {
      return ResponseEntity.ok(emailService.getEmailReport(emailId));
    }
    catch (Exception ex) {
      return handleInternalServerError(ex);
    }
  }
}

3.4. Cucumber configuration

We can use @RunWith and @CucumberOptions to enable cucumber on our project and specify the directory containing future files.

CucumberIntegrationTest.java

@RunWith(Cucumber.class)
@CucumberOptions(features = "src/test/resources")
public class CucumberIntegrationTest {

}

We also need to make Cucumber be able to load Spring Boot context and start our Spring Boot application via @CucumberContextConfiguration and @SpringBootTest. Finally, the most important thing is defining cucumber steps using Gherkin language which maps with a Java method.

SpringIntegrationTest.java

@CucumberContextConfiguration
@SpringBootTest(classes = Application.class, webEnvironment = WebEnvironment.DEFINED_PORT)
public class SpringIntegrationTest {

  @Autowired
  private ContactRepository contactRepository;
  @Autowired
  private EmailRepository emailRepository;
  @Autowired
  private EmailContactRepository emailContactRepository;

  @Autowired
  private RestTemplate restTemplate;

  private final String baseUrl = "http://localhost:8080";

  @Before
  public void initialization() {
    emailContactRepository.deleteAll();
    emailRepository.deleteAll();
    contactRepository.deleteAll();
  }

  @Given("^the below contacts exists")
  public void createContact(DataTable table) {
    List<List<String>> rows = table.asLists(String.class);
    List<Contact> contacts = new ArrayList<>();

    for (int i = 1; i < rows.size(); i++) {
      List<String> columns = rows.get(i);
      contacts.add(Contact.builder()
        .name(columns.get(0))
        .email(columns.get(1))
        .unsubscribed(Boolean.parseBoolean(columns.get(2)))
        .build());
    }
    contactRepository.saveAll(contacts);
  }

  @When("^the email with title \"([^\"]*)\" and content \"([^\"]*)\" is sent to below emails")
  public void sendEmail(String title, String content, DataTable table) {
    Set<String> emails = table.asLists(String.class).stream().map(columns -> columns.get(0)).collect(
      Collectors.toSet());
    SendEmailDto sendEmailDto = SendEmailDto.builder()
      .content(content)
      .title(title)
      .emails(emails)
      .build();

    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_JSON);
    HttpEntity<SendEmailDto> request = new HttpEntity<>(sendEmailDto, headers);
    restTemplate.postForObject(baseUrl + "/emails/send", request, Void.class);
  }

  @When("^contact \"([^\"]*)\" clicks an email with title \"([^\"]*)\"")
  public void openEmail(String email, String emailTitle) {
    Long emailId = emailRepository.findByTitleIn(Set.of(emailTitle)).get(0).getId();
    Long contactId = contactRepository.findByEmailIn(Set.of(email)).iterator().next().getId();
    URI uri = UriComponentsBuilder.fromHttpUrl(baseUrl).path("/emails/{emailId}/contacts/{contactId}/open").buildAndExpand(emailId, contactId).toUri();
    restTemplate.postForObject(uri, null, Void.class);
  }

  @Then("^the below report of email with title \"([^\"]*)\" exists")
  public void getReport(String emailTitle, DataTable table) {
    Long emailId = emailRepository.findByTitleIn(Set.of(emailTitle)).get(0).getId();
    URI uri = UriComponentsBuilder.fromHttpUrl(baseUrl).path("/emails/{emailId}/report").buildAndExpand(emailId).toUri();
    EmailReportDto actualData = restTemplate.getForObject(uri, EmailReportDto.class);

    EmailReportDto expectedData = new EmailReportDto();
    List<String> columns = table.asLists(String.class).get(1);
    expectedData.setTitle(columns.get(0));
    expectedData.setContent(columns.get(1));
    expectedData.setTotalContacts(Integer.parseInt(columns.get(2)));
    expectedData.setTotalSent(Integer.parseInt(columns.get(3)));
    expectedData.setTotalError(Integer.parseInt(columns.get(4)));
    expectedData.setTotalOpened(Integer.parseInt(columns.get(5)));

    Assertions.assertTrue(expectedData.compare(actualData));
  }
}

Let’s dive deeper into each method inside the above class.

initialization: clear all data before running any test to make sure all of our test scenario is isolated from each other using Junit @Before

createContact: this method maps with Cucumber step @Given(“^the below contacts exists”) in order to set up contact data, note that the Datatable parameter is mapped with the table defined in Cucumber scenarios as below which is an amazing feature of Cucumber.

Given the below contacts exists
      | Name  | Email           |
      | Thoai | thoai@gmail.com |
      | Ti    | ti@gmail.com    |

sendEmail, openEmail, getReport: these methods map with our RESTful APIs defined above, we use regular expressions to map each API’s parameter such as email title, and email contact. Below is an example step which maps with sendEmail(String title, String content, DataTable table) method.

When the email with title "hello" and content "how are you" is sent to below emails
      | thoai@gmail.com |
      | ti@gmail.com    |

The getReport method maps with a “then” step which is the expected result of a test scenario, so we use Junit Assertions to verify the email report must be displayed as our expectation.

3.5. Write BDD tests

Now, we’ve enough configurations to integrate Cucumber into our application and it’s time to write BDD tests. Imagine the business requirement of our application is really simple, we only need to implement the sending email feature, tracking open email event feature, and email report feature. So we will create two test scenarios to cover this requirement:

  1. Send an email to contacts:
    • Given: setup contact data
    • When: Send an email to two contacts with email is “thoai@gmail.com” or “ti@gmail.com”
    • Then: the expected result is that the email report must contain one row with “Total Contacts” = 2 and “Total Sent” = 2
  2. Send an email to contacts, then two contacts open an email
    • Given: Setup contact data
    • When: Send an email to two contacts with email is “thoai@gmail.com” or “ti@gmail.com”
    • When: Two contacts above click this email
    • Then: the expected result is that the email report must contain one row with “Total Contacts” = 2 and “Total Opened” = 2

send-email.feature

Feature: send email

  Background:
    Given the below contacts exists
      | Name  | Email           |
      | Thoai | thoai@gmail.com |
      | Ti    | ti@gmail.com    |

  Scenario: send an email to contacts
    When the email with title "hello" and content "how are you" is sent to below emails
      | thoai@gmail.com |
      | ti@gmail.com    |
    Then the below report of email with title "hello" exists
      | Title | Content     | Total Contacts | Total Sent | Total Error | Total Opened |
      | hello | how are you | 2              | 2          | 0           | 0            |

  Scenario: send an email to contacts, then two contacts open email
    When the email with title "hello" and content "how are you" is sent to below emails
      | thoai@gmail.com |
      | ti@gmail.com    |
    And contact "ti@gmail.com" clicks an email with title "hello"
    And contact "thoai@gmail.com" clicks an email with title "hello"
    Then the below report of email with title "hello" exists
      | Title | Content     | Total Contacts | Total Sent | Total Error | Total Opened |
      | hello | how are you | 2              | 0          | 0           | 2            |

On the feature above, we use “background” to define the setup contact step which is common to all the tests in the feature file. It allows us to add some context to the scenarios for a feature where it is defined.

Imagine, that after MVP1, the business adds a new requirement: allow contacts to unsubscribe from email, and if we send an email to an unsubscribed contact, we will mark it as “Error” on the email report. A product owner guy (or BA, whoever works closely with the business side) will look at this feature file and update it to a new version as below.

send-email.feature

Feature: send email

  Background:
    Given the below contacts exists
      | Name  | Email           | Unsubscribed |
      | Thoai | thoai@gmail.com | false        |
      | Ti    | ti@gmail.com    | false        |
      | Teo   | teo@gmail.com   | true         |

  Scenario: send an email to contacts
    When the email with title "hello" and content "how are you" is sent to below emails
      | thoai@gmail.com |
      | ti@gmail.com    |
    Then the below report of email with title "hello" exists
      | Title | Content     | Total Contacts | Total Sent | Total Error | Total Opened |
      | hello | how are you | 2              | 2          | 0           | 0            |

  Scenario: send an email to contacts, then two contacts open email
    When the email with title "hello" and content "how are you" is sent to below emails
      | thoai@gmail.com |
      | ti@gmail.com    |
    And contact "ti@gmail.com" clicks an email with title "hello"
    And contact "thoai@gmail.com" clicks an email with title "hello"
    Then the below report of email with title "hello" exists
      | Title | Content     | Total Contacts | Total Sent | Total Error | Total Opened |
      | hello | how are you | 2              | 0          | 0           | 2            |

  Scenario: send an email to unsubscribed contact
    When the email with title "hello" and content "how are you" is sent to below emails
      | thoai@gmail.com |
      | teo@gmail.com   |
    Then the below report of email with title "hello" exists
      | Title | Content     | Total Contacts | Total Sent | Total Error | Total Opened |
      | hello | how are you | 2              | 1          | 1           | 0            |

The new version of the feature file adds a new Unsubscribed column into the contact table and a new scenario about sending email to an unsubscribed contact. This feature test file can’t be passed unless we add new code to implement the new requirement. This is how BDD works to ensure we’re building the right function the business needs.

4. Conclusion

Within this article, we’ve looked at the BDD concept as well as how BDD is applied to software development process. We’ve also dug into Cucumber and created a Spring Boot application to demonstrate how BDD is applied to reduce the gap between the technical side and the business side. This demo application can be found on GitHub.

Leave a Reply

Your email address will not be published. Required fields are marked *

Back To Top