You have a feature file. You have the Maven setup from Lesson 2. Now you need to connect them: a set of Java methods that Cucumber can call when it encounters each Gherkin step. These are step definitions — and writing your first set is the moment the BDD loop closes.
The feature file
Create src/test/resources/features/login.feature:
Feature: User Login
Scenario: Successful login with valid credentials
Given the user is on the login page
When the user enters email "standard_user@saucedemo.com"
And the user enters password "secret_sauce"
And the user clicks the login button
Then the user should be on the products pageRun the test runner now. Every step will be Undefined. Cucumber will print a Java snippet for each one — copy those snippets for the next section.
The step definitions
Create src/test/java/stepdefinitions/LoginSteps.java:
package stepdefinitions;
import io.cucumber.java.en.And;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class LoginSteps {
WebDriver driver;
@Given("the user is on the login page")
public void theUserIsOnTheLoginPage() {
driver = new ChromeDriver();
driver.get("https://www.saucedemo.com");
}
@When("the user enters email {string}")
public void theUserEntersEmail(String username) {
driver.findElement(By.id("user-name")).sendKeys(username);
}
@And("the user enters password {string}")
public void theUserEntersPassword(String password) {
driver.findElement(By.id("password")).sendKeys(password);
}
@And("the user clicks the login button")
public void theUserClicksTheLoginButton() {
driver.findElement(By.id("login-button")).click();
}
@Then("the user should be on the products page")
public void theUserShouldBeOnTheProductsPage() {
assertTrue(
driver.getCurrentUrl().contains("inventory"),
"Expected products page URL but was: " + driver.getCurrentUrl()
);
driver.quit();
}
}Run the test runner again. The scenario executes: Chrome opens, navigates to Sauce Demo, logs in, and asserts the URL. One green scenario.
How the mapping works
Cucumber matches each Gherkin step to a step definition by comparing the step text against the annotation value. The annotation text is a Cucumber Expression — a pattern that can include parameter placeholders.
@When("the user enters email {string}") matches any step text that fits the pattern with a quoted value: When the user enters email "alice@test.com" passes alice@test.com as the username parameter.
The four built-in parameter types you'll use constantly:
| Placeholder | Matches | Java type |
|---|---|---|
{string} | text in double quotes | String |
{int} | an integer (no quotes) | int |
{float} | a decimal number | float |
{word} | a single word (no spaces, no quotes) | String |
So When the user adds {int} items to the cart with step text When the user adds 3 items to the cart passes 3 as an int.
The annotation imports
There's one trap: the annotation @And and @When and @Given are just convenience labels for readability. Cucumber treats them all the same — it's the text that matters, not whether you used @When or @And. You can annotate every method @When and it works fine. But the labels help readers understand the role of each step without reading the body.
Import them from io.cucumber.java.en.* for English steps. For other languages, the package differs (e.g., io.cucumber.java.fr.* for French).
Parameter count must match
The method signature must have exactly as many parameters as there are {placeholder} expressions in the annotation. If the step text has two {string} parameters:
When the user enters email "alice@test.com" and password "secret"The annotation and method must reflect that:
@When("the user enters email {string} and password {string}")
public void enterEmailAndPassword(String email, String password) {
...
}Mismatch between placeholder count and parameter count produces a CucumberException at startup.
The execution flow
When Cucumber can't find a step
If no step definition matches a Gherkin step, the step is Undefined. The scenario is marked as failed and Cucumber prints a snippet:
@Given("the user is on the login page")
public void theUserIsOnTheLoginPage() {
// Write code here that turns the phrase above into concrete actions
throw new io.cucumber.java.PendingException();
}Copy the snippet into a step definition class, remove the throw new PendingException(), and implement the body. The PendingException is a reminder to finish the implementation — leaving it in marks the step as Pending rather than Passing.
Keeping step definitions clean
Step definitions are wiring, not logic. The job of a step definition is to translate the Gherkin sentence into a call to the layer below — a Page Object for Selenium, a RequestSpecification for Rest Assured, a service class for integration tests. Keep them short:
// Good — delegates to a page object
@When("the user logs in with email {string} and password {string}")
public void loginAs(String email, String password) {
loginPage.loginAs(email, password);
}
// Bad — Selenium directly in the step definition
@When("the user logs in with email {string} and password {string}")
public void loginAs(String email, String password) {
driver.findElement(By.id("email")).sendKeys(email);
driver.findElement(By.id("password")).sendKeys(password);
driver.findElement(By.cssSelector("[data-testid='submit']")).click();
}The second version works but creates tight coupling between Gherkin steps and Selenium locators. When an ID changes, you hunt through step definition classes instead of page objects. Chapter 3 covers the full Page Object integration.
⚠️ Common mistakes
- Driver lifecycle in step definition fields. Creating the driver in
@Givenand quitting it in@Thenworks for one scenario but breaks when scenarios run in a different order or some scenarios skip@Then. Always manage the driver in@Before/@Afterhooks — covered in Chapter 3 Lesson 2. - Step text that's too specific to one scenario.
@When("the user enters email \"alice@test.com\"")(with the literal value in the annotation) means you need a new step definition for every email address. Use@When("the user enters email {string}")and parameterise. Step definitions should be reusable across scenarios. - Duplicate step definition text. Two methods with identical or ambiguous annotation text cause a
CucumberAmbiguousStepDefinitionsException. Ensure every step text maps to exactly one step definition.
🎯 Practice task
Wire up a full login scenario against Sauce Demo and see it go green. 40–50 minutes.
- Create
src/test/resources/features/login.featurewith the login scenario from this lesson. - Run the runner — copy all the
Undefinedsnippets Cucumber generates. - Create
stepdefinitions/LoginSteps.javaand paste in the snippets. Implement each method body using the Selenium code from this lesson. - Run again — the scenario should pass. Watch Chrome open, log in, and close.
- Add a second scenario to
login.featurefor a failed login (wrong password). What step shouldThenassert? (Hint: Sauce Demo shows an error message element.) Implement the new steps. - Run both scenarios. Both should pass (one for success, one asserting the error message is visible).
- Stretch: extract the driver creation from the step definition into a standalone
hooks/Hooks.javaclass with@Before(create driver) and@After(quit driver). Make the driver available toLoginStepsvia a constructor parameter (PicoContainer DI, covered fully in Chapter 3). Confirm both scenarios still pass.
Next chapter: Gherkin in depth — Background, Scenario Outline, data tables, and tags for organising a real test suite.