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 pageJava with Cucumber
Setup
<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.featureStep Definitions
Java Step Definitions
Selenium 3 & 4 Stable
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")); }}from behave import given, when, thenfrom 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_urlconst { 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');});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
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" )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 @loginFeature: User Login
@happy-path Scenario: Successful login ...
@negative @validation Scenario: Login with empty username ...Run specific tags:
# Javamvn test -Dcucumber.filter.tags="@smoke"mvn test -Dcucumber.filter.tags="@login and not @slow"
# Python (Behave)behave --tags=@smokebehave --tags="@login and not @slow"
# JavaScriptnpx cucumber-js --tags "@smoke"Page Object with Cucumber
Page Object for Cucumber
Selenium 3 & 4 Stable
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(); }}from selenium.webdriver.common.by import Byfrom selenium.webdriver.support.ui import WebDriverWaitfrom 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()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;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
| Practice | Description |
|---|---|
| Write declarative steps | Focus on behavior, not implementation |
| Keep scenarios short | 5-8 steps maximum |
| Use Background wisely | For common setup across scenarios |
| Reuse step definitions | Don’t duplicate step code |
| Organize with tags | Group related scenarios |
| Avoid UI details in Gherkin | ”I click login” not “I click #login-btn” |
BDD Workflow
- Discuss - Team discusses feature requirements
- Document - Write Gherkin scenarios together
- Implement - Developers create step definitions
- Test - Automate with Selenium
- Refine - Update scenarios as requirements evolve
Next Steps
- Page Object Model - Structure page interactions
- Data-Driven Testing - External test data
- pytest Integration - Python testing