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.
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
Examplestables, 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)
- Why BDD hybrid for this project (business readability + maintainability).
- The layers and the folder structure.
- One scenario end to end: feature → step → page object → assertion.
- How state is shared and parallel runs stay safe.
- Data, config and environments.
- Reports and CI.
- 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.
Related
- The Complete Cucumber BDD Tutorial
- Step Definitions & Cucumber Expressions
- The Complete Automation Framework Guide
- BDD & Cucumber Interview Questions