Featured resource
2026 Tech Forecast
2026 Tech Forecast

1,500+ tech insiders, business leaders, and Pluralsight Authors share their predictions on what’s shifting fastest and how to stay ahead.

Download the forecast
  • Lab
    • Libraries: If you want this lab, consider one of these libraries.
    • Core Tech
Labs

Writing Executable Specifications with Gherkin

A bike share company is replacing its scattered requirements documents with executable specifications before its developers rebuild the rental platform. Product owners have handed over user stories about renting a bike, returning it, applying membership discounts, and charging late fees. You work through the starter project, translating those stories into Gherkin feature files and implementing the Cucumber JVM step definitions that connect each step to the RentalStation and RentalService classes already stubbed out in the codebase, so that running the test suite proves the specification is true of the running code.

Lab platform
Lab Info
Level
Intermediate
Last updated
Nov 13, 2026
Duration
45m

Contact sales

By clicking submit, you agree to our Privacy Policy and Terms of Use, and consent to receive marketing emails from Pluralsight.
Table of Contents
  1. Challenge

    Step 1: Meet the bike share rental platform and its executable specifications

    A bike share company is rebuilding its rental platform, and before a single class gets rewritten, the team wants requirements that can't drift out of date the way its old documents did. A user story like "a rider can rent an available bike" is easy to agree on in a meeting and easy to misread six months later. Gherkin turns that same sentence into a scenario with a fixed shape — Given a starting state, When an action happens, Then an outcome is checked — and Cucumber JVM runs it against the real RentalStation and RentalService classes on every build. If the code stops matching the story, the scenario fails, not silently. Open the starter project. src/main/java/lab/RentalStation.java already tracks bikes at a station and lets a rider rent or return one. src/main/java/lab/RentalService.java holds the billing rules, membership discounts and late fees, with some of that logic still to be filled in over the coming steps. src/main/java/lab/RentalSteps.java is where Gherkin text gets connected to Java: each method is annotated @Given, @When or @Then with a Cucumber expression that matches a line of a scenario. The feature files themselves live in src/test/resources/lab/. A feature file's job is to describe behaviour in plain sentences; a step definition's job is to make one of those sentences executable. A feature file groups related scenarios under one Feature: heading. Each Scenario: inside it is a short, independent story: its Given lines set up state, its When line performs the one action under test, and its Then lines assert on the result. Cucumber matches each line against every step definition's Cucumber expression — the parts in curly braces, like {int} or {word}, capture values straight out of the sentence and pass them as method arguments. Get the wording or the parameter type wrong and Cucumber reports the step as undefined or ambiguous rather than failing the assertion, which is a different kind of problem to diagnose. Run bash runSpecs.sh in the Terminal at any point to execute both feature files through the step definitions. Cucumber prints every scenario with the step definition each line matched, and reports each scenario as passed, failed, or undefined. Add a tag, as in bash runSpecs.sh @billing, to run only the scenarios that carry it. That report is what the next four steps build toward: first a feature file with real scenarios, then structure to keep it readable as it grows, then data-driven and tagged scenarios, and finally the fixes needed when a step definition doesn't quite match what the feature file asks of it.

    info> If you get stuck, you can refer to the provided solution code for each task, available in the solution folder.

  2. Challenge

    Step 2: Translate the first rental user stories into Gherkin

    The first two user stories are mirror images of each other: a rider who finds a bike waiting, and a rider who doesn't. Both belong in src/test/resources/lab/rental.feature, and both follow the same shape: state the station's inventory in Given, name the rider's action in When, check the result in Then. Cucumber matches every line against the expressions in RentalSteps.java, so the wording has to follow them: the station {string} has {int} bikes available is written as the station "Central" has 3 bikes available, with the quoted name filling {string} and the number filling {int}. Keeping the When line identical between the two scenarios is what lets both bind to the same step definition instead of forcing you to write two. The empty-station story isn't a variation of the first scenario's assertion — it's a separate scenario with its own starting state. Writing it as its own Scenario: rather than cramming a second case into the first keeps the report readable: a failure will point at exactly the one story that broke. Both scenarios now bind to step definitions, but the methods behind them are empty, so nothing real happens when Cucumber runs them. RentalSteps.java is where that gets fixed: the @Given method stocks the station, the @When method calls RentalStation.rentBike and remembers what it got back, and the @Then methods read that stored result to make the assertions. The method bodies are ordinary Java; the only unusual part is that Cucumber decides which method runs by matching the feature file's text against each method's Cucumber expression.

  3. Challenge

    Step 3: Structure the feature file and outline repeated cases

    Both scenarios in rental.feature currently start with the same stocking step, and a third scenario about returning a bike is about to need it too. Repeating it three times means changing it three times if the starting count ever needs to move. A Background: block runs its steps before every scenario in the file automatically, so the shared setup lives in exactly one place. With the Background in place, a new scenario only needs to state what's different about it. The return-a-bike story needs a rider who already has a bike checked out, which means chaining two When steps — rent, then return — before checking the count landed back where it started. The late-fee rule scales with how many days a bike is overdue, at $1.50 per day, and the product owners described it with several near-identical examples that only differ by two numbers. Writing three almost-identical scenarios would triple the maintenance cost for one rule. A Scenario Outline writes the steps once with <placeholder> names standing in for the values that vary, and an Examples: table beneath it supplies one row of concrete values per case Cucumber should run. Because the rate is $1.50, the expected fees are decimals such as 1.50, which turns out to matter in Step 5.

  4. Challenge

    Step 4: Organize scenarios with data tables and tags

    Billing scenarios are about to need riders with different membership tiers, and a separate Given step per rider would clutter every scenario that uses them. A Gherkin data table solves that: a header row names the columns, and each row beneath becomes one record. Before a feature file can use one, though, a step definition has to accept it. Cucumber passes the table to the method as a DataTable parameter, and dataTable.asMaps() turns it into one Map per row, keyed by the header names. With the step definition in place, a single Given can register several riders. The membership discount scenario uses that to set up a gold, a silver and a basic member, then checks what each one pays on the same late fee: gold members get 20% off, silver members 10%, and basic members pay full price. Not every scenario belongs to every concern. A tag such as @billing or @inventory, written on the line directly above Scenario:, lets you run just the scenarios that matter for a change: bash runSpecs.sh @inventory skips everything else. A scenario can carry more than one tag when it genuinely spans concerns, as the full-trip scenario in Step 5 will.

  5. Challenge

    Step 5: Connect the remaining steps and diagnose Cucumber JVM failures

    The membership discount scenario has been failing since Task 4.2. Run bash runSpecs.sh @billing and read Cucumber's report: the first final-charge step fails with an assertion mismatch, expected: <12.0> but was: <0.0>, because RentalService.applyMembershipDiscount still returns 0.0. That's a different kind of failure from an undefined step: the Gherkin bound to Java correctly, and the Java gave the wrong answer. The late-fee outline has a different problem. Run bash runSpecs.sh and look at its three examples: each Then the late fee should be $1.50-style line has no step definition printed beside it, the summary lists the examples under Undefined scenarios, and Cucumber ends with a snippet for the step it expected. An undefined step means no Cucumber expression matched the line at all; an ambiguous step means more than one did. Here the cause is a parameter type: the expression in RentalSteps.java uses {int}, which only matches whole numbers, and the outline's fees have a decimal point. Every piece is now in place individually: renting, returning, registering a tier, discounting, charging a late fee. A full trip scenario doesn't introduce new logic — it strings together step wordings already proven in earlier scenarios and checks that the final number reflects every rule that applies to it at once.

  6. Challenge

    Step 6: Watch the specification prove the rental rules

    Every task is done and graded. Now run both feature files as the single report the product owners will read, filter it by concern, and watch it catch a rule that drifts. ### See every story bound to your Java

    In the Terminal, run:

    bash runSpecs.sh
    

    Cucumber prints each scenario line by line, and the comment beside each step names the Java method that ran it. When "Alex" rents a bike from station "Central" sits next to lab.RentalSteps.riderRentsABike(java.lang.String,java.lang.String), the method you filled in during Task 2.3, and the riders table points at the theFollowingRidersAreRegistered method you wrote in Task 4.1. The summary ends with 9 Scenarios (9 passed) and 37 Steps (37 passed): the three rental stories, the three late-fee examples your outline expanded into, the discount, the inventory check and the full trip. ### Run one concern

    bash runSpecs.sh @inventory
    

    Only Renting reduces inventory at each station and the full trip run, and the summary reads 2 Scenarios (2 passed). The tags you placed in Tasks 4.3 and 5.3 are the only @inventory tags in the files, so a change to inventory code can be checked without the billing stories. bash runSpecs.sh "@billing and not @inventory" narrows it the other way, to the discount scenario alone. ### Watch a rule break

    Open src/main/java/lab/RentalService.java, find return daysLate * 1.50; in calculateLateFee, and change 1.50 to 2.00. Then run:

    bash runSpecs.sh
    

    The summary drops to 9 Scenarios (5 failed, 4 passed). Each failure is an assertion mismatch in a step you wrote: the first outline row reports expected: <1.5> but was: <2.0>, the discount scenario expected: <12.0> but was: <16.0>, and the full trip expected: <4.8> but was: <6.4>. The rental and inventory scenarios still pass, because no rule they describe changed.

    Change the rate back to 1.50 before you finish. You now have requirements the build can check: plain-language stories that fail the moment the rental platform stops matching them.

About the author

Pluralsight’s AI authoring technology is designed to accelerate the creation of hands-on, technical learning experiences. Serving as a first-pass content generator, it produces structured lab drafts aligned to learning objectives defined by Pluralsight’s Curriculum team. Each lab is then enhanced by our Content team, who configure the environments, refine instructions, and conduct rigorous technical and quality reviews. The result is a collaboration between artificial intelligence and human expertise, where AI supports scale and efficiency, and Pluralsight experts ensure accuracy, relevance, and instructional quality, helping learners build practical skills with confidence.

Real skill practice before real-world application

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Learn by doing

Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.

Follow your guide

All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.

Turn time into mastery

On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.

Get started with Pluralsight