Step Definitions & Expressions
Cucumber Expressions, regex, parameter types
How Step Matching Works
When Cucumber runs a scenario, it reads each Gherkin step and searches through all your Java classes (in the glue package) for a method whose annotation matches the step text. Here’s the flow:
Gherkin Step: "When I add 5 and 3"
↓
Cucumber scans all classes in the glue package
↓
Finds: @When("I add {int} and {int}")
public void iAddAnd(int a, int b)
↓
Extracts: a = 5, b = 3
↓
Executes the method with those values
Cucumber Expressions
Cucumber 7+ introduced Cucumber Expressions as a simpler alternative to regular expressions. They’re the recommended approach:
Built-in Parameter Types
| Expression | Matches | Java Type | Example Gherkin |
|---|---|---|---|
{int} | Integers | int or Integer | I have 5 items |
{float} | Decimal numbers | float or Float | The price is 9.99 |
{string} | Text in quotes | String | I search for “laptop” |
{word} | Single word (no spaces) | String | I am on the login page |
{} | Any text (anonymous) | String | Matches anything |
{bigdecimal} | Precise decimals | BigDecimal | The total is 1234.56 |
{double} | Double-precision float | double | The rate is 3.14 |
{byte} | Byte value | byte | Status code is 1 |
{short} | Short integer | short | Port 8080 |
{long} | Long integer | long | ID is 123456789 |
Examples in Action
// {string} — captures text in double or single quotes
@When("I search for {string}")
public void iSearchFor(String query) {
searchPage.search(query);
}
// Matches: When I search for "laptop"
// Matches: When I search for 'laptop'
// {int} — captures whole numbers
@Then("I should see {int} results")
public void iShouldSeeResults(int count) {
Assert.assertEquals(searchPage.getResultCount(), count);
}
// Matches: Then I should see 10 results
// {word} — captures a single word (no quotes needed)
@Given("I am on the {word} page")
public void iAmOnPage(String pageName) {
navigator.goTo(pageName);
}
// Matches: Given I am on the login page
// Matches: Given I am on the dashboard page
// {float} — captures decimal numbers
@Then("the total should be {float}")
public void theTotalShouldBe(float total) {
Assert.assertEquals(cartPage.getTotal(), total, 0.01);
}
// Matches: Then the total should be 29.99
Optional Text & Alternation
Optional Text — Parentheses
Wrap optional words in parentheses. This lets one step definition match multiple variations:
// Matches both "I click login button" and "I click the login button"
@When("I click (the )login button")
public void iClickLoginButton() {
loginPage.clickLogin();
}
// Matches "I have 1 item" and "I have 5 items"
@Then("I have {int} item(s)")
public void iHaveItems(int count) {
Assert.assertEquals(cart.getItemCount(), count);
}
// Matches "file is present" and "file is not present"
@Then("the file is (not )present")
public void theFileIsPresent() {
// handle both cases
}
Alternation — Forward Slash
Use / to offer word choices:
// Matches "I add the item" and "I remove the item"
@When("I add/remove the item")
public void iModifyItem() {
// handle both actions
}
// Matches "color is red", "color is blue", "color is green"
@Then("color is red/blue/green")
public void colorIs() { }
// Combined with parameter capture
@When("I click the accept/reject button")
public void iClickButton() { }
Regular Expressions
Before Cucumber Expressions existed, regex was the only option. You can still use regex when you need more power. Cucumber detects regex by the presence of ^ and $ anchors:
// Regex — anchored with ^ and $
@When("^I enter \"([^\"]*)\" in the (.*) field$")
public void iEnterInField(String value, String fieldName) {
page.enterValue(fieldName, value);
}
// Matches: When I enter "john" in the username field
// Matches: When I enter "test@email.com" in the email field
// Regex with numeric capture
@Then("^I should see (\\d+) results?$")
public void iShouldSeeResults(int count) {
Assert.assertEquals(searchPage.getResultCount(), count);
}
// Matches: Then I should see 1 result
// Matches: Then I should see 10 results
Cucumber Expressions vs Regex — Comparison
| Feature | Cucumber Expression | Regex |
|---|---|---|
| Readability | Easy to read | Harder to read |
| Type safety | Built-in ({int}, {string}) | Manual casting |
| Custom types | @ParameterType | Capture groups |
| Optional text | (optional) | (?:optional)? |
| Alternation | a/b/c | `(?:a |
| Detection | No ^ or $ | Starts with ^, ends with $ |
| Recommendation | Preferred for new projects | Use when complex patterns are needed |
Custom Parameter Types
When the built-in types ({int}, {string}, etc.) aren’t enough, create your own with @ParameterType:
Boolean from Text
@ParameterType("enabled|disabled")
public Boolean status(String value) {
return "enabled".equals(value);
}
// Usage
@Then("the feature should be {status}")
public void featureShouldBe(Boolean isEnabled) {
Assert.assertEquals(isEnabled, feature.isEnabled());
}
// Matches: Then the feature should be enabled
// Matches: Then the feature should be disabled
Enum Mapping
public enum UserRole { ADMIN, USER, VIEWER }
@ParameterType("admin|user|viewer")
public UserRole role(String value) {
return UserRole.valueOf(value.toUpperCase());
}
// Usage
@Given("I am logged in as a {role}")
public void loggedInAs(UserRole role) {
loginHelper.loginAs(role);
}
// Matches: Given I am logged in as a admin
// Matches: Given I am logged in as a viewer
Object from Pattern
// Parse dates from Gherkin text
@ParameterType("\\d{4}-\\d{2}-\\d{2}")
public LocalDate date(String value) {
return LocalDate.parse(value);
}
// Usage
@Given("the report date is {date}")
public void reportDate(LocalDate date) {
reportPage.setDate(date);
}
// Matches: Given the report date is 2026-02-09
Composite Custom Type
// Parse money amounts
@ParameterType("\\$[\\d,]+\\.\\d{2}")
public BigDecimal amount(String value) {
return new BigDecimal(value.replace("$", "").replace(",", ""));
}
// Usage
@Then("the total should be {amount}")
public void totalShouldBe(BigDecimal expected) {
Assert.assertEquals(expected, cart.getTotal());
}
// Matches: Then the total should be $1,299.99
Step Definition Patterns
Pattern 1: One Step Definition Class per Feature
stepdefinitions/
├── LoginSteps.java // Steps for login.feature
├── CartSteps.java // Steps for cart.feature
├── CheckoutSteps.java // Steps for checkout.feature
└── CommonSteps.java // Shared steps used across features
Pattern 2: Steps Organized by Layer
stepdefinitions/
├── ui/
│ ├── LoginUISteps.java
│ └── CartUISteps.java
├── api/
│ ├── UserAPISteps.java
│ └── OrderAPISteps.java
└── common/
├── NavigationSteps.java
└── AssertionSteps.java
Reusable Steps
Since step definitions are global, a step defined once can be used across all feature files:
// CommonSteps.java — Reusable across all features
public class CommonSteps {
@Given("I am on the {word} page")
public void iAmOnPage(String pageName) {
navigator.navigateTo(pageName);
}
@Then("I should see a success message")
public void iShouldSeeSuccessMessage() {
Assert.assertTrue(page.isSuccessMessageDisplayed());
}
@Then("I should see an error {string}")
public void iShouldSeeError(String errorMessage) {
Assert.assertEquals(errorMessage, page.getErrorMessage());
}
@When("I wait for {int} seconds")
public void iWaitForSeconds(int seconds) {
Thread.sleep(seconds * 1000L);
}
}
# login.feature — uses CommonSteps
Scenario: Failed login
Given I am on the login page # CommonSteps
When I login with invalid password # LoginSteps
Then I should see an error "Invalid" # CommonSteps
# registration.feature — same CommonSteps
Scenario: Successful registration
Given I am on the register page # CommonSteps
When I submit the registration form # RegistrationSteps
Then I should see a success message # CommonSteps
Avoiding Ambiguous Steps
The most common error in Cucumber is the ambiguous step error. It occurs when a Gherkin step matches more than one step definition:
io.cucumber.core.exception.CucumberException:
Step "I click the button" matches more than one step definition:
iClickButton() in LoginSteps
iClickTheButton() in CommonSteps
Common Causes and Fixes
| Cause | Example | Fix |
|---|---|---|
| Duplicate definitions | Two methods with @When("I click the button") | Delete one, keep in a shared class |
| Overlapping patterns | "I am on the {word} page" and "I am on the login page" | Remove the specific one — the generic one handles it |
| Regex too broad | "^I (.*)$" matches everything | Make the regex more specific |
| Copy-paste leftovers | Same step in old and new step class | Search your glue package for duplicates |
Prevention Strategies
Step Definition Best Practices
Thin Step Definition Example
// GOOD — Step definition delegates to Page Object
@When("I login with {string} and {string}")
public void iLoginWith(String username, String password) {
loginPage.login(username, password);
}
// BAD — Step definition contains all the logic
@When("I login with {string} and {string}")
public void iLoginWith(String username, String password) {
driver.findElement(By.id("username")).clear();
driver.findElement(By.id("username")).sendKeys(username);
driver.findElement(By.id("password")).clear();
driver.findElement(By.id("password")).sendKeys(password);
driver.findElement(By.id("login-btn")).click();
new WebDriverWait(driver, Duration.ofSeconds(10))
.until(ExpectedConditions.urlContains("/dashboard"));
}
Handling Undefined Steps
When Cucumber encounters a step with no matching definition, it reports it as undefined and suggests a snippet:
Step undefined
You can implement it using the snippet(s) below:
@Given("I have {int} items in stock")
public void i_have_items_in_stock(Integer int1) {
// Write code here that turns the phrase above into concrete actions
throw new io.cucumber.java.PendingException();
}
Workflow for New Steps
- Write the Gherkin scenario in the feature file
- Run with
dryRun = true - Copy the generated snippets
- Paste into the appropriate step definition class
- Replace
PendingExceptionwith real implementation - Switch to
dryRun = falseand run