Skip to main content
SeleniumDecoded

Cucumber and BDD

Write behavior-driven tests using Cucumber with Gherkin syntax and Selenium.

Selenium 3 & 4 Stable

Cucumber enables Behavior-Driven Development (BDD) by writing tests in plain English using Gherkin syntax. This bridges the gap between technical and non-technical team members.

Gherkin Syntax

Gherkin uses keywords to structure test scenarios:

Feature: User Login
As a registered user
I want to log into my account
So that I can access my dashboard
Background:
Given I am on the login page
Scenario: Successful login with valid credentials
When I enter username "testuser"
And I enter password "testpass"
And I click the login button
Then I should be redirected to the dashboard
And I should see a welcome message
Scenario: Failed login with invalid password
When I enter username "testuser"
And I enter password "wrongpass"
And I click the login button
Then I should see an error message "Invalid credentials"
And I should remain on the login page

Java with Cucumber

Setup

pom.xml
<dependencies>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java</artifactId>
<version>7.14.0</version>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-junit-platform-engine</artifactId>
<version>7.14.0</version>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.15.0</version>
</dependency>
</dependencies>

Project Structure

src/
├── test/
│ ├── java/
│ │ ├── steps/
│ │ │ └── LoginSteps.java
│ │ ├── pages/
│ │ │ └── LoginPage.java
│ │ ├── hooks/
│ │ │ └── Hooks.java
│ │ └── runner/
│ │ └── TestRunner.java
│ └── resources/
│ └── features/
│ └── login.feature

Step Definitions

Java Step Definitions
Selenium 3 & 4 Stable
steps/LoginSteps.java
package steps;
import io.cucumber.java.en.*;
import org.openqa.selenium.WebDriver;
import pages.LoginPage;
import static org.junit.jupiter.api.Assertions.*;
public class LoginSteps {
private WebDriver driver;
private LoginPage loginPage;
public LoginSteps() {
this.driver = Hooks.getDriver();
this.loginPage = new LoginPage(driver);
}
@Given("I am on the login page")
public void iAmOnTheLoginPage() {
loginPage.navigate();
}
@When("I enter username {string}")
public void iEnterUsername(String username) {
loginPage.enterUsername(username);
}
@When("I enter password {string}")
public void iEnterPassword(String password) {
loginPage.enterPassword(password);
}
@When("I click the login button")
public void iClickTheLoginButton() {
loginPage.clickLogin();
}
@Then("I should be redirected to the dashboard")
public void iShouldBeRedirectedToDashboard() {
assertTrue(driver.getCurrentUrl().contains("/dashboard"));
}
@Then("I should see a welcome message")
public void iShouldSeeWelcomeMessage() {
assertTrue(loginPage.isWelcomeMessageDisplayed());
}
@Then("I should see an error message {string}")
public void iShouldSeeErrorMessage(String expectedMessage) {
String actualMessage = loginPage.getErrorMessage();
assertEquals(expectedMessage, actualMessage);
}
@Then("I should remain on the login page")
public void iShouldRemainOnLoginPage() {
assertTrue(driver.getCurrentUrl().contains("/login"));
}
}
steps/login_steps.py
from behave import given, when, then
from pages.login_page import LoginPage
@given('I am on the login page')
def step_on_login_page(context):
context.login_page = LoginPage(context.driver)
context.login_page.navigate()
@when('I enter username "{username}"')
def step_enter_username(context, username):
context.login_page.enter_username(username)
@when('I enter password "{password}"')
def step_enter_password(context, password):
context.login_page.enter_password(password)
@when('I click the login button')
def step_click_login(context):
context.login_page.click_login()
@then('I should be redirected to the dashboard')
def step_redirected_to_dashboard(context):
assert '/dashboard' in context.driver.current_url
@then('I should see a welcome message')
def step_see_welcome_message(context):
assert context.login_page.is_welcome_displayed()
@then('I should see an error message "{message}"')
def step_see_error_message(context, message):
actual = context.login_page.get_error_message()
assert message == actual
@then('I should remain on the login page')
def step_remain_on_login(context):
assert '/login' in context.driver.current_url
steps/login.steps.js
const { Given, When, Then } = require('@cucumber/cucumber');
const { expect } = require('chai');
const LoginPage = require('../pages/LoginPage');
let loginPage;
Given('I am on the login page', async function() {
loginPage = new LoginPage(this.driver);
await loginPage.navigate();
});
When('I enter username {string}', async function(username) {
await loginPage.enterUsername(username);
});
When('I enter password {string}', async function(password) {
await loginPage.enterPassword(password);
});
When('I click the login button', async function() {
await loginPage.clickLogin();
});
Then('I should be redirected to the dashboard', async function() {
const url = await this.driver.getCurrentUrl();
expect(url).to.include('/dashboard');
});
Then('I should see a welcome message', async function() {
const displayed = await loginPage.isWelcomeDisplayed();
expect(displayed).to.be.true;
});
Then('I should see an error message {string}', async function(message) {
const actual = await loginPage.getErrorMessage();
expect(actual).to.equal(message);
});
Then('I should remain on the login page', async function() {
const url = await this.driver.getCurrentUrl();
expect(url).to.include('/login');
});
Steps/LoginSteps.cs
using TechTalk.SpecFlow;
using NUnit.Framework;
using OpenQA.Selenium;
[Binding]
public class LoginSteps
{
private readonly IWebDriver _driver;
private readonly LoginPage _loginPage;
public LoginSteps(ScenarioContext context)
{
_driver = context.Get<IWebDriver>("Driver");
_loginPage = new LoginPage(_driver);
}
[Given(@"I am on the login page")]
public void GivenIAmOnTheLoginPage()
{
_loginPage.Navigate();
}
[When(@"I enter username ""(.*)""")]
public void WhenIEnterUsername(string username)
{
_loginPage.EnterUsername(username);
}
[When(@"I enter password ""(.*)""")]
public void WhenIEnterPassword(string password)
{
_loginPage.EnterPassword(password);
}
[When(@"I click the login button")]
public void WhenIClickTheLoginButton()
{
_loginPage.ClickLogin();
}
[Then(@"I should be redirected to the dashboard")]
public void ThenIShouldBeRedirectedToDashboard()
{
Assert.That(_driver.Url, Does.Contain("/dashboard"));
}
[Then(@"I should see an error message ""(.*)""")]
public void ThenIShouldSeeAnErrorMessage(string message)
{
Assert.That(_loginPage.GetErrorMessage(), Is.EqualTo(message));
}
}

Hooks (Setup/Teardown)

Cucumber Hooks
Selenium 3 & 4 Stable
hooks/Hooks.java
package hooks;
import io.cucumber.java.*;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
public class Hooks {
private static WebDriver driver;
@BeforeAll
public static void beforeAll() {
driver = new ChromeDriver();
driver.manage().window().maximize();
}
@AfterAll
public static void afterAll() {
if (driver != null) {
driver.quit();
}
}
@Before
public void before(Scenario scenario) {
// Runs before each scenario
System.out.println("Starting: " + scenario.getName());
}
@After
public void after(Scenario scenario) {
// Take screenshot on failure
if (scenario.isFailed()) {
byte[] screenshot = ((TakesScreenshot) driver)
.getScreenshotAs(OutputType.BYTES);
scenario.attach(screenshot, "image/png", "Screenshot");
}
}
public static WebDriver getDriver() {
return driver;
}
}
# features/environment.py (Behave hooks)
from selenium import webdriver
def before_all(context):
context.driver = webdriver.Chrome()
context.driver.maximize_window()
def after_all(context):
context.driver.quit()
def before_scenario(context, scenario):
print(f"Starting: {scenario.name}")
def after_scenario(context, scenario):
# Take screenshot on failure
if scenario.status == 'failed':
context.driver.save_screenshot(
f"screenshots/{scenario.name}.png"
)
support/hooks.js
const { Before, After, BeforeAll, AfterAll, Status } = require('@cucumber/cucumber');
const { Builder } = require('selenium-webdriver');
const fs = require('fs');
let driver;
BeforeAll(async function() {
driver = await new Builder().forBrowser('chrome').build();
await driver.manage().window().maximize();
});
AfterAll(async function() {
if (driver) {
await driver.quit();
}
});
Before(async function(scenario) {
this.driver = driver;
console.log(`Starting: ${scenario.pickle.name}`);
});
After(async function(scenario) {
// Take screenshot on failure
if (scenario.result.status === Status.FAILED) {
const screenshot = await driver.takeScreenshot();
this.attach(screenshot, 'image/png');
}
});
// Hooks/Hooks.cs (SpecFlow)
using TechTalk.SpecFlow;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
[Binding]
public class Hooks
{
private readonly ScenarioContext _context;
public Hooks(ScenarioContext context)
{
_context = context;
}
[BeforeScenario]
public void BeforeScenario()
{
IWebDriver driver = new ChromeDriver();
driver.Manage().Window.Maximize();
_context.Set(driver, "Driver");
}
[AfterScenario]
public void AfterScenario()
{
var driver = _context.Get<IWebDriver>("Driver");
// Screenshot on failure
if (_context.TestError != null)
{
var screenshot = ((ITakesScreenshot)driver).GetScreenshot();
screenshot.SaveAsFile($"screenshots/{_context.ScenarioInfo.Title}.png");
}
driver.Quit();
}
}

Scenario Outline (Data-Driven)

Feature: Login Validation
Scenario Outline: Login with various credentials
Given I am on the login page
When I enter username "<username>"
And I enter password "<password>"
And I click the login button
Then I should see "<result>"
Examples:
| username | password | result |
| testuser | testpass | dashboard |
| admin | admin123 | dashboard |
| invalid | wrong | Invalid credentials |
| testuser | | Password is required |
| | testpass | Username is required |

Tags for Test Organization

@smoke @login
Feature: User Login
@happy-path
Scenario: Successful login
...
@negative @validation
Scenario: Login with empty username
...

Run specific tags:

Terminal window
# Java
mvn test -Dcucumber.filter.tags="@smoke"
mvn test -Dcucumber.filter.tags="@login and not @slow"
# Python (Behave)
behave --tags=@smoke
behave --tags="@login and not @slow"
# JavaScript
npx cucumber-js --tags "@smoke"

Page Object with Cucumber

Page Object for Cucumber
Selenium 3 & 4 Stable
pages/LoginPage.java
package pages;
import org.openqa.selenium.*;
import org.openqa.selenium.support.ui.*;
public class LoginPage {
private WebDriver driver;
private WebDriverWait wait;
// Locators
private By usernameInput = By.id("username");
private By passwordInput = By.id("password");
private By loginButton = By.id("login-btn");
private By errorMessage = By.css(".error-message");
private By welcomeMessage = By.css(".welcome");
public LoginPage(WebDriver driver) {
this.driver = driver;
this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
}
public void navigate() {
driver.get("https://example.com/login");
}
public void enterUsername(String username) {
wait.until(ExpectedConditions.visibilityOfElementLocated(usernameInput))
.sendKeys(username);
}
public void enterPassword(String password) {
driver.findElement(passwordInput).sendKeys(password);
}
public void clickLogin() {
driver.findElement(loginButton).click();
}
public String getErrorMessage() {
return wait.until(ExpectedConditions.visibilityOfElementLocated(errorMessage))
.getText();
}
public boolean isWelcomeMessageDisplayed() {
return wait.until(ExpectedConditions.visibilityOfElementLocated(welcomeMessage))
.isDisplayed();
}
}
pages/login_page.py
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class LoginPage:
URL = "https://example.com/login"
# Locators
USERNAME_INPUT = (By.ID, "username")
PASSWORD_INPUT = (By.ID, "password")
LOGIN_BUTTON = (By.ID, "login-btn")
ERROR_MESSAGE = (By.CSS_SELECTOR, ".error-message")
WELCOME_MESSAGE = (By.CSS_SELECTOR, ".welcome")
def __init__(self, driver):
self.driver = driver
self.wait = WebDriverWait(driver, 10)
def navigate(self):
self.driver.get(self.URL)
def enter_username(self, username):
self.wait.until(EC.visibility_of_element_located(self.USERNAME_INPUT))
self.driver.find_element(*self.USERNAME_INPUT).send_keys(username)
def enter_password(self, password):
self.driver.find_element(*self.PASSWORD_INPUT).send_keys(password)
def click_login(self):
self.driver.find_element(*self.LOGIN_BUTTON).click()
def get_error_message(self):
element = self.wait.until(
EC.visibility_of_element_located(self.ERROR_MESSAGE)
)
return element.text
def is_welcome_displayed(self):
element = self.wait.until(
EC.visibility_of_element_located(self.WELCOME_MESSAGE)
)
return element.is_displayed()
pages/LoginPage.js
const { By, until } = require('selenium-webdriver');
class LoginPage {
constructor(driver) {
this.driver = driver;
this.url = 'https://example.com/login';
// Locators
this.usernameInput = By.id('username');
this.passwordInput = By.id('password');
this.loginButton = By.id('login-btn');
this.errorMessage = By.css('.error-message');
this.welcomeMessage = By.css('.welcome');
}
async navigate() {
await this.driver.get(this.url);
}
async enterUsername(username) {
const element = await this.driver.wait(
until.elementLocated(this.usernameInput),
10000
);
await element.sendKeys(username);
}
async enterPassword(password) {
await this.driver.findElement(this.passwordInput).sendKeys(password);
}
async clickLogin() {
await this.driver.findElement(this.loginButton).click();
}
async getErrorMessage() {
const element = await this.driver.wait(
until.elementLocated(this.errorMessage),
10000
);
return element.getText();
}
async isWelcomeDisplayed() {
const element = await this.driver.wait(
until.elementLocated(this.welcomeMessage),
10000
);
return element.isDisplayed();
}
}
module.exports = LoginPage;
Pages/LoginPage.cs
using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;
public class LoginPage
{
private readonly IWebDriver _driver;
private readonly WebDriverWait _wait;
private const string Url = "https://example.com/login";
// Locators
private By UsernameInput => By.Id("username");
private By PasswordInput => By.Id("password");
private By LoginButton => By.Id("login-btn");
private By ErrorMessage => By.CssSelector(".error-message");
private By WelcomeMessage => By.CssSelector(".welcome");
public LoginPage(IWebDriver driver)
{
_driver = driver;
_wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));
}
public void Navigate() => _driver.Navigate().GoToUrl(Url);
public void EnterUsername(string username)
{
_wait.Until(d => d.FindElement(UsernameInput)).SendKeys(username);
}
public void EnterPassword(string password)
{
_driver.FindElement(PasswordInput).SendKeys(password);
}
public void ClickLogin() => _driver.FindElement(LoginButton).Click();
public string GetErrorMessage()
{
return _wait.Until(d => d.FindElement(ErrorMessage)).Text;
}
public bool IsWelcomeDisplayed()
{
return _wait.Until(d => d.FindElement(WelcomeMessage)).Displayed;
}
}

Best Practices

PracticeDescription
Write declarative stepsFocus on behavior, not implementation
Keep scenarios short5-8 steps maximum
Use Background wiselyFor common setup across scenarios
Reuse step definitionsDon’t duplicate step code
Organize with tagsGroup related scenarios
Avoid UI details in Gherkin”I click login” not “I click #login-btn”

BDD Workflow

  1. Discuss - Team discusses feature requirements
  2. Document - Write Gherkin scenarios together
  3. Implement - Developers create step definitions
  4. Test - Automate with Selenium
  5. Refine - Update scenarios as requirements evolve

Next Steps