BDD Hybrid Framework: Architecture, POM, Data, Config & Parallel Execution

A BDD hybrid framework combines Cucumber's business-readable scenarios with the engineering of a normal automation framework — Page Object Model, externalised data and configuration, reusable utilities, parallel runs and CI. Gherkin describes what the system should do; Java code underneath does the how. This guide shows the layers, the code that connects them (including the part most tutorials get wrong: sharing the browser between step classes), and how to explain it in an interview.

Advertisement

What Makes It "Hybrid"

BDD hybrid = Cucumber + POM + data-driven testing + reusable utilities + CI. It isn't tied to a single approach: business people read the feature files, while page objects, helpers and config keep the code maintainable.

Some teams also add a keyword-driven layer, but most BDD hybrid frameworks don't — describe the pieces your framework really has. You may also find How to Explain Your Automation Framework in an Interview useful.

Why not plain BDD or plain POM?

  • Only Cucumber — Selenium code ends up inside step definitions, duplicated across steps.
  • Only POM — maintainable code, but the business can't read or review the tests.
  • Together — readable scenarios and maintainable code.

The Stack

Java, Selenium WebDriver, Cucumber (Gherkin), TestNG or JUnit as the runner, Maven, PicoContainer for dependency injection, Extent or Allure reports, Log4j2, Git and Jenkins.

The Layers

Layer Contains Rule
Features .feature files in Gherkin Business behavior only — no clicks, no locators
Step definitions Java methods matched to Gherkin steps Thin: call page methods, assert results
Page objects Locators and actions per page All Selenium code lives here (and in utilities)
Context & hooks Shared scenario state; @Before/@After One browser per scenario, cleaned up after
Utilities & config Driver factory, waits, config reader, data readers, screenshots Reusable, no business logic
Runner @CucumberOptions + TestNG/JUnit Selects features, glue, tags and reports
src/test/java/com/bank/
├── runners/      TestRunner.java
├── steps/        LoginSteps.java, TransferSteps.java
├── hooks/        Hooks.java
├── context/      TestContext.java
├── pages/        LoginPage.java, DashboardPage.java
└── utils/        DriverFactory.java, ConfigReader.java, ExcelReader.java
src/test/resources/
├── features/     login.feature, transfer.feature
├── config/       qa.properties, staging.properties
└── testdata/     users.xlsx

The Code That Connects the Layers

1. Feature file

Feature: Login

  Scenario Outline: Login with different users
    Given the user is on the login page
    When the user logs in as "<username>" with password "<password>"
    Then the "<page>" page should be displayed

    Examples:
      | username      | password     | page     |
      | standard_user | secret_sauce | Products |

2. A shared context — one browser per scenario

Steps for one scenario are often spread across several classes (login steps, transfer steps). They must share the same browser. With the cucumber-picocontainer dependency, Cucumber creates one TestContext per scenario and injects it into every step class and hook that asks for it:

public class TestContext {
    private WebDriver driver;

    public WebDriver driver() {
        if (driver == null) driver = DriverFactory.create(ConfigReader.get("browser"));
        return driver;
    }

    public void quit() {
        if (driver != null) {
            driver.quit();
            driver = null;
        }
    }
}

This replaces the static driver many tutorials use — which breaks as soon as scenarios run in parallel.

3. Hooks

public class Hooks {
    private final TestContext context;

    public Hooks(TestContext context) {
        this.context = context;
    }

    @Before
    public void openApp() {
        context.driver().get(ConfigReader.get("baseUrl"));
    }

    @After
    public void tearDown(Scenario scenario) {
        if (scenario.isFailed()) {
            byte[] png = ((TakesScreenshot) context.driver())
                    .getScreenshotAs(OutputType.BYTES);
            scenario.attach(png, "image/png", "failure");
        }

        context.quit();
    }
}

4. Step definitions — thin, calling page objects

public class LoginSteps {
    private final TestContext context;

    public LoginSteps(TestContext context) {
        this.context = context;
    }   // injected by PicoContainer

    @Given("the user is on the login page")
    public void onLoginPage() {
        Assert.assertTrue(new LoginPage(context.driver()).isDisplayed());
    }

    @When("the user logs in as {string} with password {string}")
    public void logIn(String username, String password) {
        new LoginPage(context.driver()).login(username, password);
    }

    @Then("the {string} page should be displayed")
    public void pageShown(String title) {
        Assert.assertEquals(
                new ProductsPage(context.driver()).title(),
                title
        );
    }
}

5. Page object — where Selenium lives

public class LoginPage {
    private final WebDriver driver;
    private final WebDriverWait wait;

    private final By username = By.id("user-name");
    private final By password = By.id("password");
    private final By loginBtn = By.id("login-button");

    public LoginPage(WebDriver driver) {
        this.driver = driver;
        this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    }

    public boolean isDisplayed() {
        return wait.until(
                ExpectedConditions.visibilityOfElementLocated(loginBtn)
        ).isDisplayed();
    }

    public void login(String user, String pass) {
        driver.findElement(username).sendKeys(user);
        driver.findElement(password).sendKeys(pass);

        wait.until(
                ExpectedConditions.elementToBeClickable(loginBtn)
        ).click();
    }
}

If you prefer @FindBy, call PageFactory.initElements(driver, this) in the constructor — a page object with @FindBy fields and no initElements() throws a NullPointerException.

6. Runner

@CucumberOptions(
    features = "src/test/resources/features",
    glue     = {"com.bank.steps", "com.bank.hooks"},
    tags     = "@smoke",
    plugin   = {
        "pretty",
        "html:target/cucumber.html",
        "json:target/cucumber.json"
    }
)
public class TestRunner extends AbstractTestNGCucumberTests {

    @Override
    @DataProvider(parallel = true)            // run scenarios in parallel
    public Object[][] scenarios() {
        return super.scenarios();
    }
}

Tags choose what runs — see Cucumber Tags & Test Execution. Thread count comes from TestNG's data-provider-thread-count (see the TestNG Cheat Sheet).

Test Data and Configuration

  • Small data sets: Scenario Outline Examples tables, visible to the business.
  • Larger data sets: Excel (Apache POI), JSON or a database, read by a utility and referenced by key from the step — e.g. Given the "premium" customer logs in. See Data-Driven Testing.
  • Configuration: one properties file per environment (qa.properties, staging.properties) chosen with -Denv=qa; secrets from environment variables or Jenkins credentials, never committed. Automated UI tests normally don't run against production except for read-only smoke checks.

Execution Flow

Code pushed to Git
        ↓
Jenkins job triggered  →  mvn test -Denv=qa -Dcucumber.filter.tags="@regression"
        ↓
TestRunner selects features by tag
        ↓
Hooks open the browser (one per scenario)
        ↓
Steps → page objects → Selenium actions
        ↓
Hooks attach screenshots on failure and close the browser
        ↓
Cucumber / Extent reports published in Jenkins

Explore how Page Object, Factory and other design patterns fit together in the Design Patterns Visualizer.

Advantages and Disadvantages

Advantages Disadvantages
Business-readable scenarios and living documentation Extra layer to maintain (feature files + step definitions)
Reusable page objects and steps Step wording must be governed, or duplicates and ambiguous steps grow
Data and config outside the code Slower to set up than a plain TestNG framework
Tag-based smoke/regression runs in CI Little value if the business never reads the scenarios

More on the pitfalls: BDD Challenges in Real Projects.

The Interview Answer

Short version (30 seconds)

"Our framework is a BDD hybrid: Cucumber feature files describe the business scenarios, thin step definitions call Page Object methods where all the Selenium code lives, and utilities handle the driver, waits, config and test data. Each scenario gets its own browser through a context injected with PicoContainer, hooks attach screenshots on failure, and Jenkins runs smoke and regression suites by tag, in parallel."

Longer version (4–5 minutes)

  1. Why BDD hybrid for this project (business readability + maintainability).
  2. The layers and the folder structure.
  3. One scenario end to end: feature → step → page object → assertion.
  4. How state is shared and parallel runs stay safe.
  5. Data, config and environments.
  6. Reports and CI.
  7. One challenge you solved (duplicate steps, flaky waits, shared state) and the result.

From Real Projects

BDD and TDD are part of the frameworks I've worked with. What makes BDD valuable is the conversation: scenarios written in plain language that testers, developers and product people all understand, which is also the idea behind Testsigma's plain-English test creation. Keep each scenario focused on one behavior and let the step definitions call your existing page and business-library code. Step definitions should stay thin and call page and business-library methods.

📚 Official documentation: Selenium Grid documentation · Selenium documentation

FAQs

What is a BDD hybrid framework?

Cucumber BDD combined with Page Object Model, data-driven testing, reusable utilities and CI — business-readable scenarios on top of maintainable automation code. You may also find Page Object Model Selenium useful.

What design pattern does it use?

Mainly the Page Object Model, plus a factory for creating drivers and dependency injection for sharing scenario state.

How do step definitions connect to page classes?

They receive the shared context (with the driver) through their constructor, create page objects with that driver, and call page methods.

How do you share the WebDriver between step definition classes?

With dependency injection such as PicoContainer: a context object holds the driver, and Cucumber injects the same instance into every step class for that scenario.

Where is the Selenium code written?

In page classes and utility classes — never directly in step definitions.

How is parallel execution handled?

The TestNG runner's scenarios() DataProvider is set to parallel, and each scenario gets its own browser through the injected context, so threads never share a driver.