- Lab
-
Libraries: If you want this lab, consider one of these libraries.
- Core Tech
Gherkin Scenarios in Automation
The retail test guild maintains checkout automation in SpecFlow (.NET) and inventory automation in Behave (Python), but both teams keep drifting the same scenarios apart by hand. You are building the Java-based Scenario Registry the guild's Jenkins pipeline calls before either stack runs: it parses the shared .feature text once, feeds identical scenarios into stand-in SpecFlow and Behave binding adapters, keeps setup hooks out of the Background and application code, lets engineers slice a run by nested capability tags, tracks when a scenario last passed Three Amigos review, and turns the run results into a Cucumber-style report scored for coverage and maintainability.
Lab Info
Table of Contents
-
Challenge
Step 1: Meet the Scenario Registry
Two stacks, one drifting truth
The retail test guild automates checkout in SpecFlow (.NET) and inventory in Behave (Python). Both teams started from the same hand-written Gherkin scenarios, but every time one team tweaks a step to fix a binding, the other team's copy quietly falls out of sync. Six months in,
@checkoutin SpecFlow and@checkoutin Behave no longer test the same behavior, and nobody notices until a release breaks.The fix isn't a new automation stack — it's a Java service that sits in front of both: parse the
.featuretext exactly once, hand identical scenarios to each stack's binding layer, and let every downstream concern (hooks, tags, review status, reporting) work off that one shared representation. ## What you will buildAcross the next four steps you will build the Scenario Registry, one file at a time:
- Step 2 — a
FeatureParserthat turns raw feature text intoScenariorecords, plus SpecFlow-style and Behave-style adapters that both replay the same parsed scenarios. - Step 3 — a
HookRegistryandBackgroundRunnerthat keep setup hooks and shared Background steps cleanly separated from the checkout application logic they surround. - Step 4 — a
CapabilityIndexand a nested booleanTagExpressionMatcherthat let an engineer run just the slice of the suite a capability filter selects. - Step 5 — a
ReviewTrackerthat records Three Amigos sign-off and flags stale scenarios, feeding aReportPublisherthat scores a run for coverage.
Each file builds on the
Scenariorecord the parser produces in Step 2, so the registry stays one coherent pipeline rather than five unrelated exercises. ## The shared building blockEvery file you write operates on the same small record:
public record Scenario(String name, List<String> tags, List<String> steps) {}A
Scenariocarries its name, the capability and review tags it was written with (@checkout,@smoke, and so on), and the orderedGiven/When/Thenlines that make up its body. This record is already in the workspace — you won't edit it — but recognizing its three fields now will make every task ahead easier to reason about, sinceparse(), both adapters, the hooks layer, the tag matcher, and the reporter all read or produce lists of it.info> If you get stuck, you can refer to the provided solution code for each task, available in the
solutionfolder. - Step 2 — a
-
Challenge
Step 2: Parse and reuse a shared feature file across bindings
From feature text to structured scenarios
Before either stack can run anything, the guild's raw
.featurefiles — plain text with@taglines,Scenario:lines, and step lines — need to become data a Java program can work with. That conversion happens exactly once, in one file, so both stacks are guaranteed to see identical scenarios rather than two hand-parsed approximations of the same text. ## Binding one scenario list to two stacksWith
parse()producing a sharedList<Scenario>, the next question is proof: does that one list actually drive both automation stacks identically? Each stack has its own binding conventions — SpecFlow logs steps joined with semicolons, Behave with pipes — but the scenarios themselves must be the same objects, untouched between the two adapters. ## The second stack, same scenariosNow repeat the proof from the other side. The Behave-style adapter takes the exact same
Scenarioobjects — no separate parse, no separate feature file — and only its formatting differs. If both adapters run cleanly off oneList<Scenario>, the guild's drift problem is solved at the source. -
Challenge
Step 3: Keep hooks, Background, and application logic apart
Hooks are not Background steps
It's tempting to treat "stuff that runs before a scenario" as one bucket, but Cucumber-style frameworks separate two distinct concepts. Hooks are framework-level setup/teardown (spinning up a browser, resetting a database) that never appears in the feature file and knows nothing about scenario content. Background steps are Gherkin steps the author wrote, shared by every scenario in a feature, and they read like any other step. Conflating them is how teams end up with hooks that quietly call application methods directly — exactly the coupling the guild wants to eliminate. ## Background steps run for everyone
With hooks handled as pure bracketing around an action, the second half of the split is making sure Background steps — the ones authored in the feature file — always execute ahead of a scenario's own steps, in a way that's driven by the shared step list rather than by any one binding's convention. ## Application logic stays reachable only through steps
The last piece of the separation is the checkout logic itself. It must do real work — sum a basket, apply a discount — but it must never be something a hook calls directly. By keeping
CheckoutServicea plain calculation with no dependency onHookRegistryorBackgroundRunner, the only path into it is through a bound step, which is exactly the boundary the guild's two stacks need to respect. -
Challenge
Step 4: Tag scenarios by capability and filter with nested expressions
Organizing by business capability
As the shared scenario list grows past a handful of features, engineers stop wanting "run everything" and start wanting "run everything for checkout" or "run everything tagged smoke." The first step toward that is indexing: grouping every parsed scenario under each business-capability tag it carries, so a lookup by tag is a map access instead of a full scan. ## Nested tag expressions
A flat index answers "which scenarios have
@checkout" but not "which scenarios are@checkoutand (@smokeor@regression)" — the kind of nested filter a real CI pipeline actually needs. That requires treating the expression as a small boolean grammar: tokens are tag lookups,and/or/notcombine them, and parentheses override the default evaluation order. ## From matching to runningA matcher that answers
true/falseper scenario is only useful once something applies it across the whole suite and reports which scenarios survived. That's the last piece: a runner that filters by the expression and produces the run log the guild's pipeline actually reads. -
Challenge
Step 5: Track Three Amigos review and publish the run report
Three Amigos and living specifications
A Gherkin scenario is only trustworthy as a "living specification" if a business owner, a developer, and a tester actually reviewed it together — the Three Amigos practice. Automation without a review trail just runs whatever text happens to be in the file, stale or not. The registry needs to remember, per scenario, when that review last happened. ## Overdue reviews
Recording a review date only pays off once something checks it against the calendar. A scenario reviewed eight months ago is functionally the same risk as one never reviewed at all, so the tracker needs a single rule that catches both cases the same way. ## Publishing a scored report
The guild's Jenkins job doesn't want a pass/fail count — it wants a Cucumber-style report that also communicates how much of what ran is actually backed by a current Three Amigos review. That means the report's coverage figure isn't "scenarios executed," it's "scenarios executed and currently reviewed," which is what makes the number meaningful for maintainability rather than just volume.
-
Challenge
Step 6: Watch one feature file drive the whole registry
Every task is done and graded. Now run the registry end to end: one block of feature text with four scenarios goes through both stacks, a hooked checkout scenario, a tag filter, and the review-scored report, all in code you wrote.
See one parse feed both stacks
Open the Terminal tab and run:
bash runApp.shSection
1. One feature file, two stacksstarts withParsed 4 scenarios once.and then prints every scenario twice. Compare the two lines for the same scenario:[SpecFlow] Restock a shelf: Given a shelf is empty; When stock arrives; And the shelf is scanned; Then the shelf is full [Behave] Restock a shelf -> Given a shelf is empty | When stock arrives | And the shelf is scanned | Then the shelf is fullThe names and steps match exactly,
Andline included, because yourSpecFlowAdapter(Task 2.2) andBehaveAdapter(Task 2.3) both read the oneList<Scenario>yourFeatureParser(Task 2.1) produced. Only the formatting differs. Edit a step in the feature text and both stacks get the change, which is the drift problem from Step 1 solved at the source. ### Watch hooks bracket a scenario without touching checkout codeIn the same output, section
2. Hooks around "Apply a discount code"printsstep: Given the store is openahead of the scenario's own three steps. That's yourBackgroundRunner(Task 3.2) putting the shared Background first. Next comesbound step: checkoutTotal([20.0, 10.0], "SAVE10") = 27.0: yourCheckoutService(Task 3.3), called from the scenario's step.The last line is everything your
HookRegistry(Task 3.1) did:Hook log: [before:Apply a discount code, after:Apply a discount code]It recorded the bracket and ran the scenario once. It never called the checkout code itself, so setup, Background, and application logic each stay in their own layer. ### Slice the run and watch coverage follow
Section
4. Cucumber report as of 2024-03-01of that run lists all four scenarios and ends withCoverage: 50% | Scenarios: 4.Pay with a gift card [OVERDUE]was last reviewed on 2023-12-01, more than 30 days earlier, andFlag low stock [OVERDUE]was never reviewed. Now run only one slice of the suite:bash runApp.sh "@checkout and not @smoke"Section 3 prints only
Pay with a gift card: RAN, and the report drops toCoverage: 0% | Scenarios: 1. Try a nested expression:bash runApp.sh "@smoke and (@checkout or @inventory)"This time
Apply a discount code: RANandRestock a shelf: RAN, and the report readsCoverage: 100% | Scenarios: 2. YourTagExpressionMatcher(Task 4.2) evaluated thenotand the parentheses, yourTaggedRunner(Task 4.3) kept the matches in file order, and yourReportPublisher(Task 5.3) scored only what ran, using the dates yourReviewTracker(Tasks 5.1 and 5.2) holds. The same suite can be fully reviewed for one slice and not at all for another, and the report tells you which.To rerun every check, use
bash runTest.sh 'Step*Test'. You now have one registry that keeps two stacks in sync, keeps hooks out of application code, and tells the guild which slice of the suite it can trust.
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.