Guided Walkthrough — Features, Step Definitions, POM, Reports

12 min read

This walkthrough builds the SecureBank BDD suite end to end. Work through it in order — each phase builds on the previous. The target application is Parabank, a public banking demo that covers all five feature areas from the brief. If you're working against a different app, the architecture is identical; adjust locators to match your target.

Phase 1: Maven setup and TestContext

Start with a new Maven project. The complete pom.xml dependency block:

<properties>
    <cucumber.version>7.18.0</cucumber.version>
    <selenium.version>4.21.0</selenium.version>
    <allure.version>2.25.0</allure.version>
</properties>
 
<dependencies>
    <dependency>
        <groupId>io.cucumber</groupId>
        <artifactId>cucumber-java</artifactId>
        <version>${cucumber.version}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>io.cucumber</groupId>
        <artifactId>cucumber-junit-platform-engine</artifactId>
        <version>${cucumber.version}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.platform</groupId>
        <artifactId>junit-platform-suite</artifactId>
        <version>1.10.2</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>io.cucumber</groupId>
        <artifactId>cucumber-picocontainer</artifactId>
        <version>${cucumber.version}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.seleniumhq.selenium</groupId>
        <artifactId>selenium-java</artifactId>
        <version>${selenium.version}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>io.github.bonigarcia</groupId>
        <artifactId>webdrivermanager</artifactId>
        <version>5.9.1</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>io.rest-assured</groupId>
        <artifactId>rest-assured</artifactId>
        <version>5.4.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>io.qameta.allure</groupId>
        <artifactId>allure-cucumber7-jvm</artifactId>
        <version>${allure.version}</version>
        <scope>test</scope>
    </dependency>
</dependencies>
 
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.2.5</version>
        </plugin>
    </plugins>
</build>

TestContext — the shared state object injected by PicoContainer into every step definition class and the Hooks class:

package context;
 
import io.github.bonigarcia.wdm.WebDriverManager;
import io.restassured.response.Response;
import io.restassured.specification.RequestSpecification;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
 
public class TestContext {
    private WebDriver driver;
    private RequestSpecification requestSpec;
    private Response lastApiResponse;
    private String lastCreatedAccountId;
 
    public WebDriver getDriver() {
        if (driver == null) {
            ChromeOptions options = new ChromeOptions();
            if (Boolean.parseBoolean(System.getProperty("headless", "false"))) {
                options.addArguments("--headless=new");
            }
            WebDriverManager.chromedriver().setup();
            driver = new ChromeDriver(options);
            driver.manage().window().maximize();
        }
        return driver;
    }
 
    public void quitDriver() {
        if (driver != null) { driver.quit(); driver = null; }
    }
 
    public RequestSpecification getRequestSpec() { return requestSpec; }
    public void setRequestSpec(RequestSpecification spec) { this.requestSpec = spec; }
 
    public Response getLastApiResponse() { return lastApiResponse; }
    public void setLastApiResponse(Response r) { this.lastApiResponse = r; }
 
    public String getLastCreatedAccountId() { return lastCreatedAccountId; }
    public void setLastCreatedAccountId(String id) { this.lastCreatedAccountId = id; }
}

Phase 2: Hooks and runner

package hooks;
 
import context.TestContext;
import io.cucumber.java.After;
import io.cucumber.java.Before;
import io.cucumber.java.Scenario;
import io.restassured.RestAssured;
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.http.ContentType;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
 
public class Hooks {
    private final TestContext context;
 
    public Hooks(TestContext context) { this.context = context; }
 
    @Before("@ui")
    public void setupBrowser() { context.getDriver(); }
 
    @Before("@api")
    public void setupApiClient() {
        String baseUrl = System.getProperty("apiBaseUrl", "https://parabank.parasoft.com/parabank");
        context.setRequestSpec(new RequestSpecBuilder()
            .setBaseUri(baseUrl)
            .setContentType(ContentType.JSON)
            .build());
        RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
    }
 
    @After
    public void afterScenario(Scenario scenario) {
        if (scenario.isFailed() && context.getDriver() != null) {
            byte[] shot = ((TakesScreenshot) context.getDriver())
                .getScreenshotAs(OutputType.BYTES);
            scenario.attach(shot, "image/png", scenario.getName());
        }
        context.quitDriver();
    }
}

Runner class:

package runners;
 
import org.junit.platform.suite.api.*;
import static io.cucumber.junit.platform.engine.Constants.*;
 
@Suite
@IncludeEngines("cucumber")
@SelectClasspathResource("features")
@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "stepdefinitions,hooks")
@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME,
    value = "pretty, " +
            "html:target/cucumber-reports/report.html, " +
            "json:target/cucumber-reports/report.json, " +
            "io.qameta.allure.cucumber7jvm.AllureCucumber7Jvm")
public class RunCucumberTest { }

Phase 3: Authentication feature

@regression @ui
Feature: Authentication
 
  @smoke
  Scenario: Successful login with valid credentials
    Given the user is on the Parabank login page
    When the user logs in with "john" and "demo"
    Then the user should see the account overview page
 
  Scenario: Login fails with wrong password
    Given the user is on the Parabank login page
    When the user logs in with "john" and "wrongpassword"
    Then the user should see a login error message
 
  Scenario: User can log out after logging in
    Given the user is logged into Parabank as "john" with password "demo"
    When the user logs out
    Then the user should be on the login page

LoginPage.java:

package pages;
 
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import java.time.Duration;
 
public class LoginPage {
    private final WebDriver driver;
    private final WebDriverWait wait;
    private static final String URL = "https://parabank.parasoft.com/parabank/index.htm";
 
    public LoginPage(WebDriver driver) {
        this.driver = driver;
        this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    }
 
    public void navigateTo() { driver.get(URL); }
 
    public void loginAs(String username, String password) {
        driver.findElement(By.name("username")).sendKeys(username);
        driver.findElement(By.name("password")).sendKeys(password);
        driver.findElement(By.cssSelector("[value='Log In']")).click();
    }
 
    public boolean isErrorDisplayed() {
        return !driver.findElements(By.cssSelector(".error")).isEmpty();
    }
 
    public boolean isOnLoginPage() {
        return driver.getCurrentUrl().contains("index.htm");
    }
}

AuthSteps.java:

package stepdefinitions;
 
import context.TestContext;
import io.cucumber.java.en.*;
import pages.LoginPage;
import static org.junit.jupiter.api.Assertions.*;
 
public class AuthSteps {
    private final TestContext context;
    private final LoginPage loginPage;
 
    public AuthSteps(TestContext context) {
        this.context = context;
        this.loginPage = new LoginPage(context.getDriver());
    }
 
    @Given("the user is on the Parabank login page")
    public void onLoginPage() { loginPage.navigateTo(); }
 
    @When("the user logs in with {string} and {string}")
    public void loginAs(String user, String pass) { loginPage.loginAs(user, pass); }
 
    @Given("the user is logged into Parabank as {string} with password {string}")
    public void alreadyLoggedIn(String user, String pass) {
        loginPage.navigateTo();
        loginPage.loginAs(user, pass);
    }
 
    @Then("the user should see the account overview page")
    public void verifyDashboard() {
        assertTrue(context.getDriver().getCurrentUrl().contains("overview"),
            "Expected overview page but was: " + context.getDriver().getCurrentUrl());
    }
 
    @Then("the user should see a login error message")
    public void verifyError() {
        assertTrue(loginPage.isErrorDisplayed(), "No error message was displayed");
    }
 
    @When("the user logs out")
    public void logout() {
        context.getDriver().findElement(
            org.openqa.selenium.By.linkText("Log Out")).click();
    }
 
    @Then("the user should be on the login page")
    public void verifyLoginPage() {
        assertTrue(loginPage.isOnLoginPage(), "Not on login page");
    }
}

Phase 4: Fund Transfer feature with Scenario Outline

@regression @ui
Feature: Fund Transfer
 
  Background:
    Given the user is logged into Parabank as "john" with password "demo"
 
  @smoke
  Scenario: Transfer between own accounts succeeds
    Given the user navigates to the transfer funds page
    When the user transfers 50 from the first account to the second account
    Then a transfer confirmation should be displayed
 
  Scenario Outline: Transfer validation
    Given the user navigates to the transfer funds page
    When the user attempts to transfer <amount> between accounts
    Then the transfer result should be "<result>"
 
    Examples:
      | amount | result       |
      | 50     | confirmed    |
      | 0      | error        |
      | -10    | error        |
      | 99999  | error        |

Phase 5: The complete execution flow

Step 1 of 8

Maven project scaffolded

pom.xml with all 9 dependencies. Package tree: stepdefinitions, pages, hooks, runners, context. Feature files in src/test/resources/features/.

Phase 6: GitHub Actions pipeline

Create .github/workflows/bdd.yml:

name: BDD Test Suite
 
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
 
jobs:
  test:
    runs-on: ubuntu-latest
 
    steps:
      - uses: actions/checkout@v4
 
      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'
 
      - name: Run BDD regression suite
        run: |
          mvn verify \
            -Dcucumber.filter.tags="@regression and not @wip" \
            -Dheadless=true \
            -Dcucumber.execution.parallel.enabled=true \
            -Dcucumber.execution.parallel.config.fixed.parallelism=2
        continue-on-error: true
 
      - name: Load Allure history
        uses: actions/checkout@v4
        if: always()
        continue-on-error: true
        with:
          ref: gh-pages
          path: gh-pages
 
      - name: Generate Allure report
        uses: simple-elf/allure-report-action@v1
        if: always()
        with:
          allure_results: target/allure-results
          allure_history: allure-history
          keep_reports: 20
 
      - name: Publish report to GitHub Pages
        if: always()
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: allure-history
 
      - name: Fail build if tests failed
        if: steps.test.outcome == 'failure'
        run: exit 1

Key design decisions in the pipeline:

  • continue-on-error: true on the test step lets the report-publishing steps run even when scenarios fail
  • Fail build if tests failed at the end ensures the pipeline correctly reports failure to the PR status
  • keep_reports: 20 retains 20 builds of Allure history — trend charts require history

Completing the remaining features

The account overview, transaction history, and bill payment features follow the same pattern as authentication and fund transfer:

  1. Write the feature file with 3–5 scenarios following the Gherkin best practices from Chapter 2
  2. Create the page object with locators specific to the target application
  3. Create the step definition class with constructor injection of TestContext
  4. Run in isolation: mvn test -Dtest=RunCucumberTest -Dcucumber.filter.tags="@feature-name"
  5. Confirm green, then add to the full regression run

For API scenarios (where Parabank exposes REST endpoints), use the Rest Assured pattern from Chapter 4 Lesson 2: build RequestSpecification in @Before("@api"), store Response in TestContext, assert in @Then steps tagged @api.

Validation checklist

Before declaring the capstone complete, verify each item:

  • mvn test -Dcucumber.filter.tags="@smoke" — all smoke tests green, under 3 minutes
  • mvn test -Dcucumber.filter.tags="@regression and not @wip" -Dheadless=true — all green
  • Parallel run with 4 threads — no NoSuchSessionException or race conditions
  • target/cucumber-reports/report.html exists and shows all scenarios
  • allure serve target/allure-results shows feature breakdown with step details
  • Deliberately fail one scenario — confirm screenshot is embedded in the Allure report
  • GitHub Actions pipeline runs and publishes the report to GitHub Pages

Next lesson: self-assessment, stretch goals, and where to go from here.

// tip to track lessons you complete and pick up where you left off across devices.