Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Java

Demystifying Behavior Driven Development with Cucumber-JVM

5.00/5 (2 votes)
4 Sep 2020CPOL9 min read 3.5K  
This article targets the needs of users who want to get a kick start with BDD practices and Cucumber-JVM in particular.
Behavior Driven Development is an extended approach of Traditional Test-Driven Development and an add-on to automated software testing. This article sheds light on the internal insights of BDD and Cucumber-JVM in particular.

Bit of a Theory

Keeping aside the fancy terms for end-to-end test writing such as reusability, maintainability, and scalability, I always prefer to have a simple definition for writing them. That is, test cases should be written and arranged in a way that they can run any number of times, in any sequence, and with a variety of different datasets. However, it is not as simple as it sounds. This kind of test writing approach demands different teams to collaborate to discuss product behavior from the very first day. Therefore, Behavior Driven Development is based on a fair collaboration among three amigos (Business Analysts, Developer, and Tester) to its entirety.

Intriguingly, the primary reason for the popularity of BDD testing is its non-technical, clear, and concise, plain English [or any other international language of your choice [1]] language. This way, a business owner can play a significant yet prompt role by specifying the requirement in a language which is understood not just by different teams (developers and testers) but also by the testing framework as well.

In our case of Cucumber-JVM, the commonly understandable language is Gherkin, which shapes the overall concept. Gherkin is a language with no technical barriers; it forces an entire team to write unambiguous requirement-based test specifications based on creative collaboration rather technical specifics. Therefore, BDD embraces the idea of making things go straight by making requirements clearer and direct from stakeholders to the tester.

$ cucumber --i18n help

Reference

Prerequisite

As stated initially, this article intends to exemplify the implementation of the BDD testing approach in a Java environment using Cucumber with an in-depth focus of its language Gherkin. Therefore, readers should have an intermediate knowledge of Java, including basic OOP concepts, understanding of a build tool (Maven or Gradle). Moreover, managing dependencies and usage of FileInputStream for reading out the values from properties will also be required.

Disclaimer

In case you do not fulfill these prerequisites, have some courage to do some reverse engineering to understand the written code and build some understanding on top of the ground, which is provided in this article from the Git repository.

Practical Insights

Before we dive into the practical insights by examining various examples using Gherkin Syntax and Cucumber-JVM Structure, allow me to talk to briefly showcase the high-level view of the project, which we will build by following along this article.

Though the project structure in Figure 1 is quite self explanatory, I will spend a few minutes to describe it anyway. To start with, there must be two major folders – main and test. main contains all the automation framework related code, which should be modular and scalable depending upon the application dynamics, which is under test. In contrast, the test folder is only for the test cases and their metadata.

Click to enlarge image

Syntactical Overview – Writing Tests in Gherkin

The .feature file in the test/resources folder is for the test specification, and it is recommended to have one file per feature for the sake of modularity and readability. As the main idea of writing test specifications in Gherkin is to make the overall testing understandable for all the members (especially for the ones without much tech knowledge); therefore, it has three prime syntax elements without which a test would not be marked as completed. These are, Given, When, Then. Nevertheless, the whole test case has to have a Scenario or Scenario Outline [2] at the top of the test script, defining the main goal or task of the entire script. Also, Gherkin supports one or more And statements in a test script. This way, more or less, the primary keywords of Gherkin are listed here.

  1. Feature
  2. Scenario and Scenario Outline
  3. Background
  4. Given, When, Then, And, But
  5. Example and Examples

I will leave the study and theoretical understanding to you, as the official document is quite helpful in this matter.

Scenario Vs. Scenario Outline

A Scenario is the high-level, one-liner definition of the executable test specification. It has steps which are Given, When, Then. However, if we have to run the same test case for multiple examples then our Scenario would have to repeat itself with the hard-coded values, which cannot be preferable in general.

Alternatively, a Scenario Outline can be used to repeat the same scenario with multiple data examples. Therefore, a Scenario Outline can better understand as ‘parametrized methods.’

Take the following example from its official documentation:

Example 1

Image 2

Clearly, Example1 is not a good example because redundancy is never preferable specially when it is playing only with the hard-code values.

Example 2

Image 3

In example2, things seem explanatory and logical.

Google Books Web Service

In this section, we will be looking into the code snippets which are taken from the demo project which I have created for this article. Now we will discuss the code which is written to test the functionality of a sample Web Service – Google Books.

Writing the Executable Specification

Please note that I will explain only one end-to-end Example of a test case. The rest will be on you to understand by going through the git repository of this project.

Python
// src/test/resources/GoogleBooks.feature

Feature: Google Book Searching from https://www.googleapis.com/books/v1/volumes?q={ID}

  Scenario Outline: Verify that the response status code is 200 and content type is JSON.
    Given webService endpoint is up
    When user sends a get request to webService endpoint using following details
      | ID | <ID> |
    Then verify <statusCode> and <contentType> from webService endpoint response
    Examples:
      | ID   | statusCode | contentType                       |
      | 1    | 200        | "application/json; charset=UTF-8" |
      | 9546 | 200        | "application/json; charset=UTF-8" |
      | 9    | 200        | "application/json; charset=UTF-8" |

Firstly, you will create the feature file (in the test/resources folder) for every intended feature to be tested and give it a description by using the Feature keyword. Note that we can have multiple Scenarios (or Scenarios Outlines) under a Feature keyword, which would, of course, be functionally relatable with each other.

Take a Scenario (or Scenarios Outline) as a parent code block – it has all the next statements inside its scope, so we call them steps.

Next, we have Given, When, Then, along with more than one data example. The Given keyword is ensuring that the given web service endpoint is up; this is the provided state from onwards we have to test some functionality.

Then we have a When statement, which is to test a specific user action – that is, sending get a request to the base URL of Web Service with a unique request ID. As the requested ID is unique and every page request will have different IDs, we will make it a parameter so that we can pass multiple IDs and run the underlined example scenario multiple times for each given ID.

Finally, we have a Then statement along with the Examples table. This ensures the intended behavior, which is expected once the user action specified in the When block is performed. Of course, the <statusCode> and <contentType> are the expected behavior we want to verify that is why we have passed them as parameters and defined in the Examples section. To know more about the working of Example Mapping, go through this official read.

Writing the Respective Step Definition File

Once we write executable specification (that is a test script written in Gherkin in simple words), we have to write its step definition file (that is Java code with public methods for every step written under the Scenario (or the Scenario Outline) in the feature file). Yeah, you get it right. This means you will have a separate step definition file per every feature file. In alternative and simpler words, the step definition file is a Java Class with dedicated functions for every Given, When, Then statement written in the feature file.

Java
// src/test/java/com/googlebooks/stepDefinitions/BookStepDefinition.java

@Given("webService endpoint is up")
public void webserviceEndpointIsUp() {
    requestSpecification = new RequestSpecBuilder().
            setBaseUri(prop.getProperty(BASE_URL)).
            build();
}

@When("user sends a get request to webService endpoint using following details")
public void userSendsAGetRequestToWebServiceEndpointUsingID(Map<String, String> data) {
    String ID = data.get("ID");
    System.out.println("The current ID is: " + ID);
    String pathParameters = "?q" + "=" + ID;
    response = given().
            spec(requestSpecification).
            when().
            get(pathParameters);
    booksVolumePojo = GoogleBooksBusinessLogic.getBooksVolumeResponseObject(response);
}

@Then("verify {int} and {string} from webService endpoint response")
public void verifyResponseStatusCodeAndContentTypeFromWebServiceEndpointResponse
            (int statusCode, String contentType) {
    Assert.assertEquals(statusCode, response.getStatusCode());
    Assert.assertEquals(contentType, response.getContentType());
}

The above code, the snippet is the step definition for the above given Gherkin test specification example. Take a moment to map the wording of Given in the feature file with its @Given definition in the above sample. Likewise, do the mapping for When and Then. The below image can help you do so.

Image 4

Mapping of a test from Feature file to its Step definition

Writing Cucumber Test Runner Class – The Glue Code

Up until this point, you should definitely be thinking about one major miss, which I have not talked about yet. That is the mapping code, or any bridge class, or in more Cucumber-JVM oriented terms, the glue code. We need the following glue code to make a mapping reference between feature files and the respective step definition files.

Java
// src/test/java/com/googlebooks/Runner/TestRunner.java

package com.googlebooks.Runner;

import io.cucumber.junit.Cucumber;
import io.cucumber.junit.CucumberOptions;
import org.junit.runner.RunWith;

@RunWith(Cucumber.class)
@CucumberOptions(glue = {"com.googlebooks.stepDefinitions"}, features = "src/test/resources",
                 plugin = {"pretty", "html:target/site/cucumber-pretty", 
                           "json:target/cucumber.json"})

public class TestRunner {
}

Some Extra Stuff

I have explained enough to this point to get you started with Cucumber-JVM and having your first experience of writing simple tests in Gherkin. However, I do not want you to scratch your head and wonder why there is much other stuff in the linked git repository (if you are convinced to at least look at it). Therefore, I thought of adding this section as a complementary part for you. So, please enjoy it and try to understand the few last things.

Figure 1 is for the project structure. There are a few folders that might confuse you; however, they bring modularity and scalability in our testing project (framework). Here goes the explanation of these components.

Business Logic Class

Java
// src/main/java/com/googlebooks/businessLogic/GoogleBooksBusinessLogic.java

public class GoogleBooksBusinessLogic {

    public static BooksVolume getBooksVolumeResponseObject(Response jsonResponse){

        BooksVolume booksVolumePojo = jsonResponse.getBody().as(BooksVolume.class);
        System.out.println("String representation of booksVolume response pojo " + 
                            booksVolumePojo.toString());
        return booksVolumePojo;
    }
}

The business logic layer is to cover an approach or a way of handling some internal, application/business-specific requirements. Such as conversion of data from one form to another somewhere to access it centrally. In the code above, we are converting the JSON response, which is received from our Web Service to a Java Pojo – `BooksVolume`. I will be calling this function where I need to parse the response from JSON to Java object. Please note that for demo purposes, I have used only one Example, that is why you see only one conversion here. You may have many such conversions and logic implementation in big projects. Apart from other advantages, this conversion allows me to reuse this converted Json response to Java Object in multiple test cases, which requires working on the same response. Moreover, it will make the code clean as once you get the converted Java object, the rest of the code flow will follow the same Pojo structure to make following Object-Oriented rules easy for you.

Property Reader Class

Java
// src/main/java/com/googlebooks/utils/PropertyReader.java

public class PropertyReader {
    protected static Properties prop;

    public PropertyReader() {
        try{
            prop = new Properties();
            FileInputStream ipStream = new FileInputStream(System.getProperty("user.dir") + 
                                       "/src/main/resources"
                    + "/config/config.properties");
            prop.load(ipStream);
        }
        catch (IOException exc){
            exc.printStackTrace();
        }
    }
}

What do you guess about its functionality by looking at its code and name? Of course, it is for reading out common properties that are externally configurable such as base URLs, IPs, ports, and many other variables that should not be the part of project compilation, hence keeping it in the resource folder of our codebase. Here, our configuration file is saved with an extension of .properties. Therefore, you will extend any class which will have to read properties from the PropertyReader class, such as our `BookStepDefinition` class.

Running Your Project

For the execution of your test cases, you need to run mvn test in your terminal.

Following is the build output I get by scrolling my window to its most down.

Image 5

Homework Exercises

This project is meant to design and explain to get you a kickstart with BDD and Cucumber-JVM in particular. Therefore, if you really want to write good Gherkin tests, add the following changes in the project.

  1. Add proper logging by using the logging library of your choice, such as slf4j.
  2. Add a few more scenarios – gherkin tests in the feature file and their step definition in the definition file.
  3. Add variables in properties readers if necessary.
  4. Explore reporting libraries and serenity documentation features provided by BDD. The Cucumber was initially being designed for Ruby, so give a thorough look at how Cucumber-JVM offers different implementation of BDD than BDD in Ruby.

History

  • 4th September, 2020: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)