mastering-bdd-cucumber-with-java-banner
Best Practices Selenium Test Automation

Mastering BDD with Cucumber and Java: Advanced Techniques for Scalable Test Automation

Table of Content

Foundation: Understanding the Basics of Cucumber

In this blog, we’ll explore the key concepts of Cucumber that are crucial for improving your automation framework. By grasping these fundamental ideas, you’ll be able to build a stronger, more efficient, and scalable testing framework that can adapt as your project grows.

In a previous blog post called “A Hands-On Introduction to Selenium, Cucumber, and BDD in Java” we explained how to set up Cucumber and Selenium and introduced the basics of Behavior-Driven Development (BDD) using Java. If you haven’t read it yet, we strongly suggest you do so before continuing with this blog. It provides all the information you need to start using Cucumber in your projects.

Key Components of a BDD Cucumber Framework

Behavior-Driven Development (BDD) using Cucumber relies on a few important parts that help make test automation work well and involve everyone on the team:

  • Feature Files: These are the core of Cucumber. They hold test scenarios written in Gherkin, a simple language that lets both tech and non-tech team members easily understand and add to the tests.
  • Step Definitions: These act like connectors, linking the steps written in Gherkin to the actual code that runs the automation, allowing the test scenarios to be executed.
  • Hooks: These allow you to set up actions to happen before or after scenarios or certain steps, like setting up resources or cleaning up after tests.
  • Tags: Used to label and group test scenarios, making it simple to organize, filter, and run specific tests when needed.
  • Runner Class: Serves as a link to testing tools like JUnit or TestNG, enabling the running of feature files and scenarios.
  • Gherkin Syntax: A straightforward and organized language that uses words like Given, When, Then, And, and But to explain scenarios in a way that both technical and non-technical team members can easily understand.
  • Glue Code: Connects feature files with their corresponding step definitions, ensuring that test scenarios run without issues.
  • Background: Offers a method to set up common steps used in many scenarios within a feature file, cutting down on repetition and making the tests easier to read.

Let’s take a closer look at how you can enhance each of these areas, beginning with Optimizing Feature File Management.

Optimizing Feature File Management

In BDD using Cucumber, feature files are very important. They describe how your application should work in a way that everyone, developers, testers, and business people can understand. As your project gets bigger, keeping these feature files organized becomes more important. Good organization helps avoid confusion, makes your project easier to maintain, and helps you grow your testing efforts smoothly.

Here are some helpful tips to manage your feature files well:

Adopt Clear and Consistent Naming for Better Code Quality

Feature File Naming in Cucumber:

When naming your feature files in Cucumber, it’s important to pick names that clearly describe what the feature is testing. Avoid unclear names like `test.feature` or `login.feature`. Instead, use names that show specific actions or user stories. Using consistent and clear names helps keep your project organized and makes it easier for anyone to understand what each file does.

  • Dos:
    • Use snake_case for feature file names (e.g., user_login.feature, checkout_process.feature). This makes the names easy to read and understand.
    • Make sure the name clearly explains what the feature is testing. For example, user_registration.feature tells you right away that it tests the user registration process.
    • Be consistent with your naming style. If you choose snake_case, use it for all your feature files.
  • Don’ts:
    • Avoid simple or unclear names like `test.feature` or `login.feature`. These don’t give enough information about what the test is checking.
    • Don’t use different naming styles (e.g., userLogin.feature, USER_checkout.feature), as this can be confusing and make things inconsistent.

Feature Naming in Cucumber:

Instead of using simple or general names, the titles inside the file should match the user story. This way, it’s clear what the feature is for, who it’s meant to help, and what it does.

Example:

Instead of a simple feature name like:

Feature: Login Functionality

You can structure it as a user story to add more clarity, like this:

Feature: Ecommerce Login Page
  In order to test the login functionality
  As a user
  I want to try login

This format, based on the Given-When-Then structure, defines the feature in terms of the user’s needs and provides valuable context for the team. It outlines:

  • In order to: The objective or benefit of the feature.
  • As a: The user or role that will benefit from this feature.
  • I want to: The specific functionality or behavior being tested.

Benefits of Using User Story Format:

  • Improved Clarity: Makes the feature’s intent clear for all developers, testers and business stakeholders.
  • Aligns with Agile Practices: Reflects the user-centered approach typical in Agile methodologies.
  • Better Collaboration: Helps the entire team whether technical or non-technical, understand the purpose of the feature.

Utilize the Background Section for Reusable Steps

The Background section in Cucumber is useful for setting up steps that are used in many scenarios. This way, you avoid repeating the same steps and make your feature files clearer.

Why Use the Background Section?

Reduces Repetition: Steps like logging in or going to a page only need to be written once, which avoids repeating them.

Enhances Readability: It keeps your scenarios focused on testing specific actions, while the setup steps are managed separately.

  • Do’s:
    • Put actions that are done in many scenarios, like logging in or going to a page, in the Background section.
    • Only add steps to the background if they are needed for more than one scenario.
  • Don’ts:
    • Don’t put steps in the background that are only for one scenario. Keep it for shared actions only.
    • Make the Background section clear and easy to understand so that the scenarios are still easy to follow.

Essential Considerations Before Using the Background Section:

  • Only include steps in the Background that are needed for many different situations.
  • Ensure the steps in the Background are simple and easy to follow.
  • Be careful not to add too many actions to the Background, as this can make fixing problems more difficult. Stick to the most important steps.

Example: We put the steps that we kept doing to go to the login page in the Background section. This way, we don’t have to repeat them and the feature file becomes easier to read.
For more information, please refer to the feature file below.

Feature: Ecommerce Login Page
  In order to test the login functionality
  As a user
  I want to try login scenarios
  Background:
    Given The user is on the Ecommerce Home Page
    When The user selects the "Login" option from the My Account dropdown

  @ValidScenarios @Ecommerce @login @smoke
  Scenario: Open Ecommerce Login Page Correctly
    Then The Ecommerce login form is displayed

  @ValidScenarios @Ecommerce @login @smoke
  Scenario Outline: Verify that the user can login to the application
    When The user logs in with "<email>" and "<password>"
    Then User is successfully logged into the application
  Examples:
      | email                    | password     |
      | gecebidys@mailinator.com | Password@123 |

Efficiently Organize Scenarios with Tags

Tags are a useful tool in Cucumber that help you organize your test scenarios by categorizing them. This makes it simpler to manage and run certain groups of tests. It’s especially helpful in CI/CD pipelines, where you might need to run different kinds of tests, such as quick checks, full checks, or tests for specific functions, using tags.

We created standard tags like @Smoke, @Regression, @Login, and @Ecommerce to group test scenarios and filter tests according to their purpose. For better understanding, please refer to the feature file example we looked at earlier.

Optimize Data-Driven Testing with Scenario Outline

When you have to run the same test with various data sets, using a Scenario Outline along with an Examples table is the best way to do it. This method helps you avoid repeating the same test steps and lets you test many cases quickly, without writing the same code over and over.

Action Taken:Changed several similar test scenarios into a single Scenario Outline to manage different data sets without duplicating the test steps.

Example:

  @ValidScenarios @Ecommerce @login @smoke
  Scenario Outline: Verify that the user can log in to the application
    When The user logs in with "<email>" and "<password>"
    Then User is successfully logged into the application
 Examples:
      | email                    | password     |
      | gecebidys@mailinator.com | Password@123 |
      | testuser@example.com     | Test@1234    |
      | demo.user@mail.com       | Demo@2024    |

Advantages of This Method:

  • Instead of creating many scenarios for each set of data, one scenario can manage all the data.
  • It lets you test many cases in one scenario, making your test files shorter and easier to handle.
  • The testing logic remains the same while working with different inputs, keeping your cotests clear and simple to change.

Implementing Cucumber Hook: Efficient Way to Manage WebDriver

In test automation, dealing with WebDriver instances and managing tasks that need to be done repeatedly, like setting up and cleaning up resources, can be challenging. Cucumber Hooks can help with this. They make the automation process easier by making sure that important setup steps and cleanup actions happen smoothly. In this blog, we’ll explain how to use Hooks effectively, keeping things simple and clear.

What Are Hooks in Cucumber?

In Cucumber, Hooks are special methods that begin with @Before, @After, @BeforeStep, and @AfterStep. These methods run at specific times during a test. They are very useful for tasks that need to be done before or after each test.

Here are the main Hooks:

  • @Before: This runs before each test or group of tests. It’s typically used for setting up things, such as starting the WebDriver.
  • @After: This runs after each test or group of tests. It’s used for cleaning up, such as closing the browser.
  • @BeforeStep and @AfterStep: These run before or after each step in a test. They are used less frequently, but can be helpful for tasks that need to happen at every step.

Why Use Hooks?

  • Single Location for Setup and Cleanup: Hooks allow you to place all setup and cleanup code in one place, making it easier to manage and keep organized.
  • Fresh Test Environment: They ensure each test begins with a clean slate by setting up and cleaning up before and after each test, which helps keep tests independent from each other.
  • Simpler Maintenance: By reducing repetitive code across different tests, Hooks keep your test scripts tidy, making them easier to manage and update when necessary.

Let’s see how to use Hooks for managing WebDriver in a real project.

Initialize the WebDriver

The @Before hook is used to start WebDriver and prepare the test environment.

    @Before(order = 0)
    protected void setUp() {
        // Initialize WebDriver
        driver = new ChromeDriver();
        driver.get(Property.getProperty("ecommerce.baseUrl"));
        driver.manage().window().maximize();
    }

Code Explanation:

  • Opens a Chrome browser, goes to the main website, and makes the browser window full-size.
  • Set order = 0 so this step happens first. It’s important to set up WebDriver before doing anything else.

Handle Tag-Specific Setup

Tags allow you to apply specific setup logic selectively, ensuring that certain actions are executed only for designated scenarios.

 @Before(value = "@Register", order = 1)
    protected void performRegistration() {
        headerPage.clickOnOptionFromMyAccountDropdown("Register");
        registerPage.fillRegistrationForm(new RegistrationData());
        ecommerceUtilitiesPage.clickOnContinueButton();
    }

Code Explanation:

  • This hook verifies whether the ongoing scenario includes the `@Register` tag. 
  • If it does, the hook initiates the registration process, directing to the registration page, completing the form, and then submitting it.
  • Assigning an order of 1 guarantees that this hook runs early in the sequence. It will be executed after any hooks with an order of 0 but prior to those with higher order numbers.
  • To use the @Register tag, put it above the scenario you want to register.
  • These tags help us only ask for registration when it’s needed for certain tests. This way, we don’t have to add registration steps to every test by hand, which makes the process faster. We can also make a similar tag for the login process.

Cleanup After Execution

The @After hook is used to clean up resources after each scenario, such as closing the WebDriver and freeing up memory.

 @After
    protected void tearDown() {
        if (driver != null) {
            driver.quit();  // Close the browser
            driver = null;  // Set driver to null
        }
    }

Code Explanation:

  • This code makes sure the WebDriver instance is properly closed after each test. 
  • The @After annotation shows that the tearDown method runs after every test. 
  • It checks if the driver is not empty and, if it is there, closes the browser with driver.quit(). 
  • Then, it sets the driver reference to empty, which prevents memory problems, frees up computer resources, and makes sure the driver can’t be accidentally used again in later tests.

Cucumber Hooks provide a simple and powerful way to manage WebDriver instances and repetitive tasks in your test automation framework. By using @Before for setup and @After for cleanup, you can efficiently manage the lifecycle of WebDriver and ensure tests run smoothly without unnecessary overhead.

Implementation of BasePage Class

The BasePage class is an important part of Selenium test automation. It serves as a base for common actions on different web pages, making the code more organized and easier to manage. By encouraging code reuse and easy updates, it keeps the automation system efficient and able to grow. In this blog, we’ll look at why the BasePage class is important, its part in the Page Object Model (POM), and how to set it up well.

Why BasePage Class?

The main advantage of using a BasePage class is that it helps remove repeated code, making sure everything is consistent and easy to maintain in your test system. By putting common methods and elements in the BasePage, you avoid writing the same code multiple times, which makes the test scripts simpler and easier to manage. This also makes it easier to update or change shared actions across all your tests.

But it’s important not to give the BasePage class too many tasks. Its main job should be to list shared page elements and provide basic interactions. More complicated actions, like clicking buttons, typing in text boxes, or waiting for elements to appear, are better handled by a SeleniumHelper class or other helper classes. This keeps the BasePage class simple, focused on the page structure, and ready to grow with your project.

Structure of BasePage Class

A standard BasePage class sets up important parts like WebDriver, WebDriverWait, and common ways to interact with web pages. It serves as a base for separate page objects, offering shared features.

Here’s how the BasePage is structured:

The BasePage class is an important part of Selenium test automation. It serves as a base for common actions on different web pages, making the code more organized and easier to manage. By encouraging code reuse and easy updates, it keeps the automation system efficient and able to grow. In this blog, we’ll look at why the BasePage class is important, its part in the Page Object Model (POM), and how to set it up well.

Why BasePage Class?

The main advantage of using a BasePage class is that it helps remove repeated code, making sure everything is consistent and easy to maintain in your test system. By putting common methods and elements in the BasePage, you avoid writing the same code multiple times, which makes the test scripts simpler and easier to manage. This also makes it easier to update or change shared actions across all your tests.

But it’s important not to give the BasePage class too many tasks. Its main job should be to list shared page elements and provide basic interactions. More complicated actions, like clicking buttons, typing in text boxes, or waiting for elements to appear, are better handled by a SeleniumHelper class or other helper classes. This keeps the BasePage class simple, focused on the page structure, and ready to grow with your project.

Structure of BasePage Class

A standard BasePage class sets up important parts like WebDriver, WebDriverWait, and common ways to interact with web pages. It serves as a base for separate page objects, offering shared features.

Here’s how the BasePage is structured:

package Base;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.PageFactory;
import org.openqa.selenium.support.pagefactory.AjaxElementLocatorFactory;
import utility.Constants;
import utility.SeleniumHelpers;

public class BasePage {

    protected WebDriver driver;
    protected SeleniumHelpers selenium;

    public BasePage(WebDriver driver) {
        this.driver = driver;
        selenium = new SeleniumHelpers(driver);
        PageFactory.initElements(new AjaxElementLocatorFactory(driver, Constants.PAGEFACTORY_WAIT_DURATION), this);
    }
}

Code Explanation:

  • Centralized WebDriver Management
    • When you pass the WebDriver instance to the BasePage constructor, you make sure that all page objects that extend BasePage can use the same driver instance. This helps keep things consistent.
  • SeleniumHelpers Integration
    • Using SeleniumHelpers lets you group common tasks like clicking buttons, typing text, or waiting for elements in one place. This helps avoid repeating code and makes your code easier to manage.
  • PageFactory Initialization
    • Using `PageFactory.initElements` makes sure that the WebElements you define in your page objects are set up automatically. This makes your code easier to read and maintain.
  • AjaxElementLocatorFactory
    • Using AjaxElementLocatorFactory with a customizable wait time (Constants.PAGEFACTORY_WAIT_DURATION) is a smart option. It helps find elements on the page dynamically, even if they take a while to load.

Advanced Page Object Model (POM)

The Page Object Model (POM) is a common way to design tests for automation. For smaller projects, a basic POM setup works fine. But for bigger and more complicated projects, we need more advanced methods. These techniques help cut down on repeated tasks, make updates easier, and manage growing complexity. In this blog, we’ll explore advanced POM strategies, building on the basic ideas we talked about before.

Key Features of an Advanced Page Object Model

Separation of Concerns

Advanced POM emphasizes keeping actions specific to each page separate from reusable functions. For instance, methods frequently used, like enterText() or clickOn(), are stored in a special utility class called SeleniumHelpers. Meanwhile, actions that are unique to each page remain in their own page object classes.

Enhanced Page Object Example

In the Page Object Model, classes such as LoginPage are created to represent particular pages of an application. These classes include the elements and actions specific to those pages. They inherit reusable features from a BasePage class using the “extends” keyword, which helps reduce repeated code.

Here’s a simple example showing how LoginPage extends BasePage:

package Pages.login;

import Base.BasePage;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;

public class LoginPage extends BasePage {

    public LoginPage(WebDriver driver) {
        super(driver);
    }

    @FindBy(id = "input-email")
    public WebElement emailField;

    @FindBy(id = "input-password")
    public WebElement passwordField;

    @FindBy(css = "input[value='Login']")
    public WebElement loginButton;

    public void loginToApplication(String email, String password) throws InterruptedException {
        selenium.enterText(emailField, email, true);
        selenium.enterText(passwordField, password, true);
        selenium.clickOn(loginButton);
    }
}

Advanced POM revolutionizes test automation, making your system robust and future-ready. By employing techniques such as reusable components, scenario-based strategies, and structured logic, you can ensure your automation framework is both efficient and easy to maintain.

Advanced Step Definition Techniques

What is a Step Definition?

In Cucumber, step definitions serve as the bridge between the steps written in the Gherkin syntax and the underlying automation code. They are methods that define what each step in a feature file should do when executed. Each Gherkin step (e.g., Given, When, Then) corresponds to a step definition that contains the code to be run.

For example, if a Gherkin step says “Given the user is on the login page,” the step definition method would contain the logic to navigate to the login page.

Why Cucumber Uses Step Definitions and Its Benefits.

Cucumber relies on step definitions to achieve the key goal of Behavior-Driven Development (BDD): ensuring clear communication between developers, testers, and business stakeholders.

Here are some of the reasons why step definitions are critical:

  • Separation of Concerns: Cucumber allows business stakeholders to write readable feature files while developers focus on writing the implementation code. Step definitions bridge this gap.
  • Reusability: Step definitions can be reused across different scenarios, improving maintainability and reducing redundancy.
  • Better Readability: Step definitions allow Gherkin syntax to be human-readable, while the actual automation code remains separate, making it easier to update tests.

While basic step definitions are simple to implement, more advanced techniques are essential for enhancing the flexibility, scalability, and maintainability of your test automation. Here are a few steps to take your step definitions to the next level:

Step 1. Use Data-Driven Testing with Scenario Outlines

To make our test automation code better, we’ve used Scenario Outlines in the feature file. These help the code be easier to read, work better with more tests, and be simpler to fix when needed. Instead of creating individual test cases for each sorting choice (such as “Name (A-Z)” or “Price (Low > High)”), we used Scenario Outline in Gherkin syntax. This lets us run the same test with various sets of data.

This is what it looks like:

Scenario Outline: Verify that the user can sort products using the Sort By dropdown options
    When The user selects the "<dropdownOption>" from the Sort By dropdown
    Then The products should be displayed in the correct order based on "<dropdownOption>"

    Examples:
      | dropdownOption     |
      | Name (A - Z)       |
      | Name (Z - A)       |
      | Price (Low > High) |
      | Price (High > Low) |

Why is this important?

  • We don’t have to do the same things over and over for each sorting method.
  • The test checks different sorting choices using the same steps.
  • This makes the feature file simple, easy to understand, and able to handle more tests.

Let’s look at these and see how we use them in our feature file with some examples.

Step 2. Writing Clear and Scalable Feature Files

Feature files are used to outline the business rules for testing different situations. A well-written feature file clearly explains what the user wants and helps with the automation process.

Let’s look at an example of how a user can sort products by using the “Sort By” dropdown menu.

Feature: Ecommerce Monitors Page
  In order to test the Monitors Page filter functionality
  As a user
  I want to try valid scenarios

  Background:
    Given The user is on the Ecommerce Home Page
    When The user clicks on the Shop by Category option
    And The user selects the "Desktops and Monitors" category from the Top Categories Side Bar
    Then The user should navigate to the Monitors page

  Scenario Outline: Verify that the user can sort products using the Sort By dropdown options
    When The user selects the "<dropdownOption>" from the Sort By dropdown
    Then The products should be displayed in the correct order based on "<dropdownOption>"

    Examples:
      | dropdownOption     |
      | Name (A - Z)       |
      | Name (Z - A)       |
      | Price (Low > High) |
      | Price (High > Low) |

Here are the advanced techniques used in the feature file:

  • Scenario Outline: Lets you use the same scenario with different inputs, which helps avoid repeating the same steps.  
  • Examples Table: Gives a way to test different versions of the scenario using the same steps by providing different data.  
  • Background Section: Sets up common steps that apply to all scenarios, so you don’t have to repeat them.

Step 3. Optimizing Page Object Model (POM) with Dynamic Product Data

In an optimized POM (Page Object Model), the page object class gets product information in real-time and works with different parts of the page.

Example (MonitorsPage.java):

package Pages.desktopAndMonitor;

import Base.BasePage;
import dataObjects.ProductDetails;
import dataObjects.ProductDetailsList;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;

import java.util.ArrayList;
import java.util.List;

public class MonitorsPage extends BasePage {

    public MonitorsPage(WebDriver driver) {
        super(driver);
    }

    @FindBy(xpath = "//h4[@class='title']//a")
    public List<WebElement> productTitles;
    @FindBy(xpath = "//span[@class='price-new']")
    public List<WebElement> productPrices;

    public ProductDetailsList getProductDetailsList() {
        ProductDetailsList productDataList = new ProductDetailsList();
        productDataList.setProductDetailsList(new ArrayList<>());

        for (int i = 0; i < productTitles.size(); i++) {
            ProductDetails productDetails = new ProductDetails();
            productDetails.setProductName(selenium.getText(productTitles.get(i)));

            String selectedProductPrice = selenium.getText(productPrices.get(i));
            String productPriceText = selectedProductPrice.replaceAll("[^\\d.]", "");
            double convertedProductPrice = Double.parseDouble(productPriceText);
            productDetails.setProductPrice(convertedProductPrice);

            productDataList.getProductDetailsList().add(productDetails);
        }
        return productDataList;
    }

    public void selectOptionFromSortByDropdown(String dropdownOption) {
        WebElement sortByDropdown = driver.findElement(By.id("sort-by-dropdown"));
        selenium.selectDropDownValueByText(sortByDropdown, dropdownOption);
    }
}

The MonitorsPage class is used for the monitors page on an online store. This page object class retrieves product names and prices dynamically from the page, helping keep the test automation code clean and efficient.It also lets you sort the products by choosing options from a dropdown list.

Step 4. Creating Effective Step Definitions for Test Automation

Step definitions are the bridge between Gherkin scenarios and automation code. The aim is to make the definitions simple and easy to read while covering different situations.

Example (MonitorsPageSteps.java):

package stepDefinitions.orderConfirmation.monitors;

import Pages.EcommerceProductDetailsPage;
import Pages.desktopAndMonitor.MonitorsPage;
import dataObjects.ProductDetailsList;
import org.junit.Assert;
import io.cucumber.java.en.When;
import io.cucumber.java.en.Then;

import java.util.List;

public class MonitorsPageSteps {

    MonitorsPage monitorsPage;
    EcommerceProductDetailsPage ecommerceProductDetailsPage;
    ProductDetailsList monitorsProductDataList;

    @When("The user selects the {string} from the Sort By dropdown")
    public void theUserSelectsTheDropdownOptionFromTheSortByDropdown(String dropdownOptionName) {
        monitorsPage.selectOptionFromSortByDropdown(dropdownOptionName);
    }

    @Then("The products should be displayed in the correct order based on {string}")
    public void theProductsShouldBeDisplayedInTheCorrectOrderBasedOnSelectedDropdownOption(String dropdownOptionName) {
        monitorsProductDataList = monitorsPage.getProductDetailsList();
        List<String> actualProductNames = monitorsPage.getActualProductNameList(monitorsProductDataList);
        List<Double> actualProductPrices = monitorsPage.getActualProductPriceList(monitorsProductDataList);

        switch (dropdownOptionName.toLowerCase()) {
            case "name (a to z)":
                Assert.assertEquals("The products list is not displayed in alphabetical order: A-Z",
                        ecommerceProductDetailsPage.sortListOfProductNamesInAlphabeticalOrder(actualProductNames), actualProductNames);
                break;
            case "name (z to a)":
                Assert.assertEquals("The products list is not displayed in alphabetical order: Z-A",
                        ecommerceProductDetailsPage.sortListOfProductNamesInDescendingOrder(actualProductNames), actualProductNames);
                break;
            case "price (low to high)":
                Assert.assertEquals("The products list is not displayed in order: Price (Low > High)",
                        ecommerceProductDetailsPage.sortListOfProductPricesInAscendingOrder(actualProductPrices), actualProductPrices);
                break;
            case "price (high to low)":
                Assert.assertEquals("The products list is not displayed in order: Price (High > Low)",
                        ecommerceProductDetailsPage.sortListOfProductPricesInDescendingOrder(actualProductPrices), actualProductPrices);
                break;
            default:
                throw new IllegalArgumentException("Invalid dropdown option: " + dropdownOptionName);
        }
    }
}

Advanced Techniques Used in Step Definitions: 

1. Parameterization for Flexibility and Reusability

Parameterization helps manage different test data inputs, so you don’t have to write individual tests for each sorting option. This method makes it simple to add new features without needing to change the test steps significantly.

@When("The user selects the {string} from the Sort By dropdown")
    public void theUserSelectsTheDropdownOptionFromTheSortByDropdown(String dropdownOptionName) {
        monitorsPage.selectOptionFromSortByDropdown(dropdownOptionName);
    }

Why is this important?

  • Easy to change: If we want to add more ways to sort, we just need to update the Examples table.
  • Reduced Redundancy: We don’t have to write different instructions for each sorting choice.
2.Custom Methods for Complex Operations

Instead of writing complicated logic over and over in step definitions, it’s better to put those actions into reusable methods inside the Page Object Model (POM). This makes the tests easier to maintain and understand by keeping the steps short and clear.

MonitorsPage.java

   public void selectOptionFromSortByDropdown(String dropdownOption) {
        WebElement sortByDropdown = driver.findElement(By.id("sort-by-dropdown"));
        selenium.selectDropDownValueByText(sortByDropdown, dropdownOption);
    }

Why is this important?

  • Reusability: By using a parameter for the sorting option, the method can be used in different situations.
  • Simplified Step Definitions: Step definitions stay focused on the main test scenario, without getting into the specific details of how it works.

We’ve looked at advanced ways to write step definitions that improve the quality and speed of test automation. By using data-driven techniques, reusable steps, and organized designs, we’ve found ways to create strong and easy-to-maintain tes. These techniques make complicated tests simpler and help teams work better with business needs, making sure software testing is both accurate and efficient.

Efficient Test Data Management

In test automation, managing test data properly is crucial for improving your tests and making them simpler to handle. Test data management (TDM) ensures that automated tests use the correct information every time they run. By using external sources like JSON files, Excel sheets, and databases for test data, you can keep the data separate from the test scripts. This approach makes the tests more adaptable and easier to manage.

In this blog, we’ll explore how to use external data sources for managing test data and demonstrate how to integrate them into your test automation system seamlessly. We’ll provide examples and steps that align with our previous practices.

JSON File for Test Data Management

JSON is a popular way to store organized information. It’s easy for people to read and for computers to understand, which makes it great for testing software.

Check out our blog post on test data management, available now on our official website. You can find it here: QA Blogs

Implementing Custom Wait Strategies

In Selenium test automation, managing waits properly is very important for running tests smoothly. Waiting for elements to load, become clickable, or appear on the page helps prevent errors caused by elements that aren’t ready yet. Although there are built-in waits like Implicit, Explicit, and Fluent Wait, sometimes you need custom waits for special situations. This blog will show you how to create custom wait strategies to handle specific conditions that standard waits can’t cover, with examples that fit your current test setup.

To learn more about custom wait strategies, check out our blog titled “Advanced Optimization Techniques for Building an Efficient Selenium Automation Framework,” where we explain these strategies in detail.

Data Factory Pattern in Cucumber

Good test data management is important for making test automation work well, easy to read, and able to be used again. This is especially true when using tools like Cucumber. The Data Factory Pattern helps by making test data objects automatically, which keeps things consistent and flexible for new needs. Let’s see how this pattern makes test automation easier when used with Cucumber.

Implement the Data Factory Pattern for Scalable Cucumber Tests

Step 1: Build a Data Object Class

Begin by creating a data class that holds the test data required for your scenarios. Using Lombok can help by minimizing repetitive code, resulting in a cleaner and more straightforward class. Define the `RegistrationDetails` data object with fields and use Lombok annotations to automatically generate getters and setters.

Using Lombok makes your code simpler and easier to manage as your framework expands. To find out how to use it in your project, check out our full guide on Lombok Unleashed: Elevating Java Efficiency with Getters, Setters, and More.

package dataObjects;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class RegistrationDetails {
    private String firstName;
    private String lastName;
    private String email;
    private long telephone;
    private String password;
    private String confirmPassword;
    private String newsletterSubscribeButton;
    private boolean privacyPolicyCheckbox;
}

Step 2: Implement the Data Factory

The RegistrationFactory class uses JavaFaker to create realistic test data automatically. Instead of using constructors, it fills in the data using a method, which makes it more adaptable for different test situations.

package dataFactory;

import com.github.javafaker.Faker;
import dataObjects.RegistrationDetails;

public class RegistrationFactory {
    public RegistrationDetails createUserRegistrationData() {
        Faker faker = new Faker();
        
        RegistrationDetails registrationDetails = new RegistrationDetails();
        registrationDetails.setFirstName(faker.name().firstName());
        registrationDetails.setLastName(faker.name().lastName());
        registrationDetails.setEmail(faker.internet().emailAddress());
        registrationDetails.setTelephone(Long.parseLong(faker.phoneNumber().phoneNumber().replaceAll("\\D", "")));
        registrationDetails.setPassword(faker.internet().password(8, 16, true, true, true));
        registrationDetails.setConfirmPassword(registrationDetails.getPassword());
        registrationDetails.setNewsletterSubscribeButton("Yes");
        registrationDetails.setPrivacyPolicyCheckbox(true);
        return registrationDetails;
    }
}

Step 3: Utilize the Factory in Cucumber Step Definitions

Incorporate the generated data into your Cucumber step definitions to ensure smooth test execution.

import dataFactory.RegistrationFactory;
import dataObjects.RegistrationDetails;

public class UserRegistrationSteps {
    RegistrationFactory registrationFactory = new RegistrationFactory();
    RegistrationDetails registrationDetails;

    @Given("A user provides valid registration details")
    public void aUserProvidesValidRegistrationDetails() {
        registrationDetails = registrationFactory.createUserRegistrationData();
        System.out.println("Generated User Details: " + registrationDetails.getFirstName() + " " + registrationDetails.getLastName());
        // Pass the registrationDetails object to application logic or UI interaction methods
    }

    @When("The user submits the registration form")
    public void theUserSubmitsTheRegistrationForm() {
        // Example: Use the registrationDetails object to fill the form
        registrationPage.enterFirstName(registrationDetails.getFirstName());
        registrationPage.enterLastName(registrationDetails.getLastName());
        registrationPage.enterEmail(registrationDetails.getEmail());
        registrationPage.enterPassword(registrationDetails.getPassword());
        registrationPage.submitForm();
    }
}

Easy integration process: The created data can be used right away to fill in form fields or send through APIs, making sure the test data is the same every time and can be used again.

Benefits of This Approach:

  • Fresh Data for Every Test: JavaFaker generates new, realistic information for each test, so we don’t have to repeat the same fixed values.
  • Adaptability: The Data Factory Pattern allows us to modify certain parts of the test data without altering the main structure, making it suitable for various scenarios.
  • Consistency: The createUserRegistrationData function can be used across multiple tests, ensuring that the way we create test data remains uniform and reliable.

Parallel Execution and Cross-Browser Testing with Cucumber

In the busy world of software development, it’s important to create strong and dependable applications. One way to do this is by using automated tests well and making sure apps work smoothly on different web browsers. In this blog, we’ll look at Parallel Execution and Cross-Browser Testing, especially when using the Cucumber framework. We’ll talk about why they’re important and how to use them properly.

Why Parallel Execution?

Parallel execution involves running several test scenarios or feature files together, rather than one by one. This method significantly reduces the overall testing time, giving faster results and improving efficiency. Here’s why it matters:

Quicker Feedback: Perfect for big test sets, it speeds up the testing process.

Efficient Resource Use: Fully utilizes the available hardware.

Improved Flexibility: Allows testing in various environments at the same time.

Implementing Parallel Execution in Cucumber

Cucumber helps you run tests together by working with tools like TestNG, JUnit, or the Maven Surefire Plugin. Here’s how to set it up with Maven:

  • Add the Maven Surefire Plugin to set up running your scenarios or feature files at the same time.
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.0.0</version>
    <configuration>
        <parallel>methods</parallel>
        <threadCount>4</threadCount>
    </configuration>
</plugin>
  • Use the command “mvn test” to run the tests. This will make different test situations happen at the same time. It helps get results quicker and makes the testing process more efficient.

Why Cross-Browser Testing?

Cross-browser testing makes sure your app works the same way on different browsers and their versions. Since there are many browsers to choose from, checking for compatibility is important to give users a smooth experience.

Advantages of Cross-Browser Testing: 

  • Better User Experience: Ensures the app looks and works the same on all supported browsers.
  • Larger Audience: Makes the app accessible to more people using different browsers.
  • Finding Bugs Early: Identifies browser-related problems during development, which helps avoid issues after the app is released.

Implementing Cross-Browser Testing in Cucumber

Creating a Driver Manager

  • A Driver Manager automatically starts browser instances according to the given settings. Here’s how to set it up in Cucumber:
public class DriverManager {
        public static WebDriver getDriver(String browser) {
                if (browser.equalsIgnoreCase("chrome")) {
                        return new ChromeDriver();
                } else if (browser.equalsIgnoreCase("firefox")) {
                        return new FirefoxDriver();
                } else if (browser.equalsIgnoreCase("edge")) {
                        return new EdgeDriver();
                } else {
                        throw new IllegalArgumentException("Unsupported browser: " + browser);
                }
        }
}

Using Hooks to Choose Browsers Automatically

Cucumber Hooks can help set up the WebDriver for each test scenario:

public class HooksSteps {

    private WebDriver driver;

    // Use a default browser if not specified
    private static final String DEFAULT_BROWSER = "chrome";

    @Before(order = 0)
    protected void setUp() {
        // Get browser from system property or use default
        String browser = System.getProperty("browser", DEFAULT_BROWSER);

        // Initialize WebDriver dynamically
        driver = DriverManager.getDriver(browser);

        // Set up browser and open the application
        driver.get(Property.getProperty("ecommerce.baseUrl"));
        driver.manage().window().maximize();
    }

    @Before(value = "@Register", order = 1)
    protected void performRegistration() {
        headerPage.clickOnOptionFromMyAccountDropdown("Register");
        registerPage.fillRegistrationForm(new RegistrationData());
        ecommerceUtilitiesPage.clickOnContinueButton();
    }

    @After
    protected void tearDown() {
        if (driver != null) {
            driver.quit();  // Close the browser
            driver = null;  // Set driver to null
        }
    }

    public WebDriver getDriver() {
        return driver;
    }
}

Code Explanation: 

The HooksSteps class handles WebDriver setup, actions for specific scenarios, and cleanup for Cucumber tests. It automatically chooses the browser based on the “browser” system property, defaulting to Chrome if no browser is specified, using the DriverManager.getDriver method. The @Before hook with order = 0 sets up the WebDriver, maximizes the browser window, and opens the application’s main URL, ensuring the browser is ready before tests start. The @Before hook with order = 1 performs actions like user registration if the scenario is tagged with @Register. The @After hook closes the browser and frees up resources to prevent memory issues. This class manages browser sessions dynamically, making it flexible and useful for testing across different browsers.

Integrate Advanced Reporting Tools for Enhanced Test Insights

Test reporting is very important in automated testing because it helps teams see what happened during the tests and find problems quickly. By using reporting tools with the Cucumber framework, it becomes easier to follow how tests are running and how well they are performing.

Why Reporting Matters:

  • Shows what happened in the tests and any possible problems.
  • Quickly finds tests that failed and why they failed.
  • Helps teams make good decisions using accurate information.
  • Keeps track of how tests are doing over time and notices any issues that come back.

Popular Reporting Tools:

  • Allure Reports
    • Creates interactive and adjustable reports.
    • Works smoothly with Cucumber.
    • Needs the allure-cucumber plugin for setup.
  • ExtentReports
    • Produces attractive HTML reports.
    • Allows real-time updates and changes to test results.
    • Connects with Cucumber using the ExtentCucumberAdapter.
  • TestNG Reports
    • Includes built-in support for HTML and XML reports.
    • Works well with Jenkins for continuous integration and deployment (CI/CD).
    • Can be used with Cucumber through the TestNG runner.
  • ReportNG
    • Simple and lightweight HTML reports.
    • Best for small to medium projects.
    • Easily integrates with Cucumber and TestNG.
  • Grafana and ELK Stack
    • Advanced data visualization using Grafana and log management with ELK.
    • Perfect for large-scale testing and performance tracking.
    • Needs custom loggers in Cucumber for integration.

Adding reporting tools to your Cucumber framework improves how well you can see test results and makes testing more efficient. Whether you use Allure, ExtentReports, or more advanced tools like Grafana and the ELK Stack, these tools help you fix problems quicker and make smarter decisions, resulting in a smoother and more effective testing process.

We’ll soon share a detailed blog explaining how to add reporting to an automation framework. For more information, you can visit our official QA Blogs page.

Streamline CI/CD Integration with Cucumber for Continuous Testing

Continuous Integration (CI) is a key part of modern agile development, automating code integration and testing. By combining CI with Cucumber, you can speed up your testing process, ensure consistent environments, and scale your tests more efficiently.

Why Use CI with Cucumber?

  • Automated tests run every time the code changes, giving you immediate feedback.
  • CI tools maintain reliable test environments for every test run.
  • CI systems can run tests in parallel, saving you time by speeding up test execution.
  • Automated tests help ensure your code is always ready for deployment.

Setting Up Continuous Integration with Cucumber

  • Choose a CI Tool: Use tools such as Jenkins, GitLab CI, or CircleCI to automatically start test runs when code is updated in the repository.
  • Run Tests Simultaneously: Set up your CI tool to run several Cucumber tests at the same time to speed up the testing process.
  • Generate Reports Automatically: Utilize tools like Allure Reports or ExtentReports to create comprehensive test reports after every run.

Combining CI with Cucumber improves your testing process by providing faster feedback, reliable results, and a smoother way to release software. To learn more about CI/CD tools, check out our guide: “Top CI/CD Tools Every QA Automation Engineer Should Know.

In this blog, we’ve shared some expert advice and methods to improve your Cucumber framework using Java. We talked about running tests at the same time to make them faster and organizing your feature files so they’re easier to read. These tips are meant to make your test automation work better and be more dependable. We also explained why connecting your tests to a CI/CD system and using reporting tools are important for keeping track of how well your tests are doing.

By following these good practices, you can make your testing process quicker, more adaptable, and simpler to manage. The main aim is not just to automate tests, but to build a strong, flexible, and efficient automation system that helps you create high-quality software consistently and quickly.

Conclusion

In this blog, we walked through some of the key techniques that will enhance your Cucumber framework with Java so that your test automation is both efficient and maintainable. We started by organizing feature files for better readability and advanced step definitions to handle complex test scenarios. With WebDriver management through hooks and leveraging the BasePage class, we ensured streamlined test execution.

We covered advanced features like Page Object Model (POM), custom wait strategies, and optimized data handling to manage test structure improvement and avoid runtime issues. Moreover, we further explored parallel execution and cross-browser execution for accelerating test cycles and combined powerful reporting tools for better insights on test performance.

Lastly, we discussed the importance of CI/CD integration, which is an important factor for maintaining a continuous testing flow. This would give quick feedback and high-quality delivery of software. By following these practices, you will not only improve the speed and efficiency of your testing but also build a more reliable and adaptable automation framework that drives consistent, high-quality results.

Witness how our meticulous approach and cutting-edge solutions elevated quality and performance to new heights. Begin your journey into the world of software testing excellence. To know more refer to Tools & Technologies & QA Services.

If you would like to learn more about the awesome services we provide,  be sure to reach out.

Happy Testing 🙂