Writing Your First Scenario and Step Definitions

9 min read

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 page

Run 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:

PlaceholderMatchesJava type
{string}text in double quotesString
{int}an integer (no quotes)int
{float}a decimal numberfloat
{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 @Given and quitting it in @Then works for one scenario but breaks when scenarios run in a different order or some scenarios skip @Then. Always manage the driver in @Before/@After hooks — 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.

  1. Create src/test/resources/features/login.feature with the login scenario from this lesson.
  2. Run the runner — copy all the Undefined snippets Cucumber generates.
  3. Create stepdefinitions/LoginSteps.java and paste in the snippets. Implement each method body using the Selenium code from this lesson.
  4. Run again — the scenario should pass. Watch Chrome open, log in, and close.
  5. Add a second scenario to login.feature for a failed login (wrong password). What step should Then assert? (Hint: Sauce Demo shows an error message element.) Implement the new steps.
  6. Run both scenarios. Both should pass (one for success, one asserting the error message is visible).
  7. Stretch: extract the driver creation from the step definition into a standalone hooks/Hooks.java class with @Before (create driver) and @After (quit driver). Make the driver available to LoginSteps via 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.

// tip to track lessons you complete and pick up where you left off across devices.