- Lab
-
Libraries: If you want this lab, consider one of these libraries.
- Core Tech
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 Info
Table of Contents
-
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 —
Givena starting state,Whenan action happens,Thenan outcome is checked — and Cucumber JVM runs it against the realRentalStationandRentalServiceclasses on every build. If the code stops matching the story, the scenario fails, not silently. Open the starter project.src/main/java/lab/RentalStation.javaalready tracks bikes at a station and lets a rider rent or return one.src/main/java/lab/RentalService.javaholds 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.javais where Gherkin text gets connected to Java: each method is annotated@Given,@Whenor@Thenwith a Cucumber expression that matches a line of a scenario. The feature files themselves live insrc/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 oneFeature:heading. EachScenario:inside it is a short, independent story: itsGivenlines set up state, itsWhenline performs the one action under test, and itsThenlines 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. Runbash runSpecs.shin 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 inbash 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
solutionfolder. -
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 inGiven, name the rider's action inWhen, check the result inThen. Cucumber matches every line against the expressions inRentalSteps.java, so the wording has to follow them:the station {string} has {int} bikes availableis written asthe station "Central" has 3 bikes available, with the quoted name filling{string}and the number filling{int}. Keeping theWhenline 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 ownScenario: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.javais where that gets fixed: the@Givenmethod stocks the station, the@Whenmethod callsRentalStation.rentBikeand remembers what it got back, and the@Thenmethods 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. -
Challenge
Step 3: Structure the feature file and outline repeated cases
Both scenarios in
rental.featurecurrently 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. ABackground: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 twoWhensteps — 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. AScenario Outlinewrites the steps once with<placeholder>names standing in for the values that vary, and anExamples: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 as1.50, which turns out to matter in Step 5. -
Challenge
Step 4: Organize scenarios with data tables and tags
Billing scenarios are about to need riders with different membership tiers, and a separate
Givenstep 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 aDataTableparameter, anddataTable.asMaps()turns it into oneMapper row, keyed by the header names. With the step definition in place, a singleGivencan 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@billingor@inventory, written on the line directly aboveScenario:, lets you run just the scenarios that matter for a change:bash runSpecs.sh @inventoryskips everything else. A scenario can carry more than one tag when it genuinely spans concerns, as the full-trip scenario in Step 5 will. -
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 @billingand read Cucumber's report: the first final-charge step fails with an assertion mismatch,expected: <12.0> but was: <0.0>, becauseRentalService.applyMembershipDiscountstill returns0.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. Runbash runSpecs.shand look at its three examples: eachThen 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 inRentalSteps.javauses{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. -
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.shCucumber 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 tolab.RentalSteps.riderRentsABike(java.lang.String,java.lang.String), the method you filled in during Task 2.3, and the riders table points at thetheFollowingRidersAreRegisteredmethod you wrote in Task 4.1. The summary ends with9 Scenarios (9 passed)and37 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 concernbash runSpecs.sh @inventoryOnly
Renting reduces inventory at each stationand the full trip run, and the summary reads2 Scenarios (2 passed). The tags you placed in Tasks 4.3 and 5.3 are the only@inventorytags 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 breakOpen
src/main/java/lab/RentalService.java, findreturn daysLate * 1.50;incalculateLateFee, and change1.50to2.00. Then run:bash runSpecs.shThe summary drops to
9 Scenarios (5 failed, 4 passed). Each failure is an assertion mismatch in a step you wrote: the first outline row reportsexpected: <1.5> but was: <2.0>, the discount scenarioexpected: <12.0> but was: <16.0>, and the full tripexpected: <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.50before 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
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.