Q11 of 21 · BDD / Cucumber
How do you structure Cucumber step definitions with the Page Object Model?
BDD / CucumberMidbddcucumberpage-object-modelarchitecture
Short answer
Short answer: Step definitions are thin glue — they call Page Object methods. Page Objects contain locators and actions. Inject Page Objects into step definition classes via DI (Java) or the World (JS), not instantiated directly in steps.
Detail
Layered architecture:
Feature File (.feature)
↓ matched by
Step Definition Class ← thin: calls PO methods, no locators
↓ uses
Page Object Class ← locators + interaction methods
↓ uses
WebDriver / Playwright page ← raw browser API
Java example with PicoContainer:
public class LoginPage {
private final WebDriver driver;
public LoginPage(WebDriver driver) { this.driver = driver; }
public void enterEmail(String email) {
driver.findElement(By.id("email")).sendKeys(email);
}
public void clickLogin() {
driver.findElement(By.id("login-btn")).click();
}
public String getErrorMessage() {
return driver.findElement(By.className("error")).getText();
}
}
public class LoginSteps {
private final LoginPage loginPage;
private final TestContext ctx;
public LoginSteps(TestContext ctx) {
this.ctx = ctx;
this.loginPage = new LoginPage(ctx.driver);
}
@When("the user logs in with {string} and {string}")
public void userLogsIn(String email, String password) {
loginPage.enterEmail(email);
loginPage.enterPassword(password);
loginPage.clickLogin();
}
}
Why this separation matters:
- Locators change → update only the Page Object, not every step definition
- Step definitions remain readable business descriptions
- Page Objects can be shared across feature files
- Easier to unit test Page Objects in isolation
// EXAMPLE
// pages/LoginPage.js
class LoginPage {
constructor(page) { this.page = page; }
async login(email, password) {
await this.page.fill('[data-testid="email"]', email);
await this.page.fill('[data-testid="password"]', password);
await this.page.click('[data-testid="login-btn"]');
}
}
// steps/loginSteps.js
When('the user logs in with {string} and {string}', async function(email, pwd) {
const loginPage = new LoginPage(this.page);
await loginPage.login(email, pwd);
});// WHAT INTERVIEWERS LOOK FOR
Three-layer architecture (feature → step def → page object). Steps should contain no locators. DI or World to share the page/driver instance.