Cucumber Step Definitions — Connecting Gherkin to Automation Code

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'")
Note: Step definitions should call page object methods, not raw Selenium/Playwright commands. Instead of 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.
Tip: Use Cucumber expressions (the {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.
Warning: Step definitions must be unique — two step definitions that match the same Gherkin text cause an ambiguity error. This commonly happens when separate step files define similar steps: 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.

🧠 Test Yourself

Why should step definitions call page object methods instead of raw Selenium/Playwright commands?