Gherkin scenarios are plain text — they do not execute anything by themselves. Step definitions are the glue code that connects each Gherkin step to automation code. When the runner encounters Given I am on the login page, it searches for a step definition that matches this text and executes the associated function. This lesson shows how to write step definitions in Python (using Behave) and JavaScript (using Cucumber.js) that bind to Selenium, Cypress, or Playwright automation.
Step Definitions — Binding Gherkin to Automation
Step definitions use regular expressions or Cucumber expressions to match Gherkin steps and extract parameters.
# ── PYTHON: Behave step definitions ──
# File: features/steps/login_steps.py
from behave import given, when, then
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
# Step: Given I am on the login page
@given('I am on the login page')
def step_visit_login(context):
context.driver.get("https://www.saucedemo.com")
WebDriverWait(context.driver, 10).until(
EC.visibility_of_element_located((By.ID, "login-button"))
)
# Step: When I enter username "{username}" and password "{password}"
@when('I enter username "{username}" and password "{password}"')
def step_enter_credentials(context, username, password):
context.driver.find_element(By.ID, "user-name").send_keys(username)
context.driver.find_element(By.ID, "password").send_keys(password)
# Step: And I click the login button
@when('I click the login button')
def step_click_login(context):
context.driver.find_element(By.ID, "login-button").click()
# Step: Then I should see the inventory page
@then('I should see the inventory page')
def step_verify_inventory(context):
WebDriverWait(context.driver, 10).until(EC.url_contains("inventory"))
assert "inventory" in context.driver.current_url
# Step: Then I should see {count:d} products displayed
@then('I should see {count:d} products displayed')
def step_verify_product_count(context, count):
products = context.driver.find_elements(By.CLASS_NAME, "inventory_item")
assert len(products) == count, f"Expected {count} products, found {len(products)}"
# Step: Then I should see an error message containing "{text}"
@then('I should see an error message containing "{text}"')
def step_verify_error(context, text):
error = WebDriverWait(context.driver, 5).until(
EC.visibility_of_element_located((By.CSS_SELECTOR, "[data-test='error']"))
)
assert text.lower() in error.text.lower(), f"'{text}' not found in '{error.text}'"
# ── JAVASCRIPT: Cucumber.js step definitions ──
# (shown as comments to avoid syntax issues in Python file)
JS_STEP_DEFINITIONS = '''
// File: features/step_definitions/login.steps.js
const { Given, When, Then } = require('@cucumber/cucumber');
const { expect } = require('chai');
Given('I am on the login page', async function () {
await this.page.goto('https://www.saucedemo.com');
await this.page.waitForSelector('#login-button');
});
When('I enter username {string} and password {string}', async function (username, password) {
await this.page.fill('#user-name', username);
await this.page.fill('#password', password);
});
When('I click the login button', async function () {
await this.page.click('#login-button');
});
Then('I should see the inventory page', async function () {
await this.page.waitForURL(/inventory/);
expect(this.page.url()).to.include('inventory');
});
Then('I should see {int} products displayed', async function (count) {
const products = await this.page.locator('.inventory_item').count();
expect(products).to.equal(count);
});
'''
print("Step Definition Pattern:")
print(" 1. Gherkin step: Given I am on the login page")
print(" 2. Regex/pattern: @given('I am on the login page')")
print(" 3. Function body: driver.get('https://...')")
print()
print(" Parameters are extracted from the Gherkin text:")
print(" Step: When I enter username \"alice\" and password \"pass123\"")
print(" Pattern: @when('I enter username \"{username}\" and password \"{password}\"')")
print(" Result: username='alice', password='pass123'")
context.driver.find_element(By.ID, "user-name").send_keys(username), the step should call context.login_page.enter_username(username). Step definitions are the translation layer between business language (Gherkin) and page-level actions (page objects). Raw automation calls in step definitions create the same maintenance problems as raw calls in test methods — duplicated locators and fragile code.{string}, {int}, {float} syntax) instead of regular expressions for parameter matching. Cucumber expressions are more readable and handle type conversion automatically: {int} passes an integer, {string} passes a string (stripping quotes). Regular expressions are more powerful but harder to read and maintain.login_steps.py has @given('I am on the login page') and navigation_steps.py also has @given('I am on the login page'). Organise step definitions by domain (login, cart, checkout) and ensure each step text maps to exactly one function.Common Mistakes
Mistake 1 — Putting raw Selenium calls in step definitions instead of page objects
❌ Wrong: Step definition contains driver.find_element(By.ID, "user-name").send_keys(username) — locators scattered across step files.
✅ Correct: Step definition calls context.login_page.enter_username(username) — locators live in the page object only.
Mistake 2 — Writing steps that are too specific to one scenario
❌ Wrong: @when('I enter standard_user as username on the SauceDemo login page') — cannot be reused.
✅ Correct: @when('I enter username "{username}" and password "{password}"') — parameterised, reusable across all login scenarios.