Acceptance Test-Driven Development — Practice Tasks¶
Category: Craftsmanship Disciplines — drive development from executable acceptance criteria agreed with the business.
10 graded hands-on tasks with full solutions in Gherkin plus Python (behave) and Java (Cucumber) step definitions. Try each before expanding the solution.
Table of Contents¶
- Task 1: User Story → Gherkin
- Task 2: Wire a Step Definition
- Task 3: Drive a Feature Outside-In
- Task 4: Fix a UI-Coupled Test (Push to the Service Layer)
- Task 5: Make an Imperative Scenario Declarative
- Task 6: Scenario Outline for a Rule Table
- Task 7: Run an Example Mapping Session
- Task 8: Isolate Scenario Test Data
- Task 9: Stub an External Dependency
- Task 10: Rebalance an Ice-Cream-Cone Suite
- Practice Tips
Task 1: User Story → Gherkin¶
Goal: Turn a vague user story into a declarative scenario with concrete examples, including a boundary case.
Given the story:
"As a member, I want to earn loyalty points on purchases, so that I'm rewarded. Members earn 1 point per dollar; orders of $50+ earn double points."
Solution
Feature: Loyalty points
As a member
I want to earn points on purchases
So that I'm rewarded for spending
Scenario Outline: Points earned per order
Given a member places an order of $<amount>
When the order completes
Then the member earns <points> points
Examples:
| amount | points | note |
| 10 | 10 | 1x below threshold |
| 49 | 49 | just below the boundary |
| 50 | 100 | exactly at threshold (2x) |
| 80 | 160 | 2x above threshold |
Task 2: Wire a Step Definition¶
Goal: Connect the scenario from Task 1 to real code at the service layer (no UI).
Solution
### Python (behave)from behave import given, when, then
from loyalty.service import LoyaltyService, Order
@given('a member places an order of ${amount:d}')
def step_order(context, amount):
context.service = LoyaltyService()
context.order = Order(amount=amount)
@when('the order completes')
def step_complete(context):
context.points = context.service.points_for(context.order)
@then('the member earns {points:d} points')
def step_assert(context, points):
assert context.points == points, f"got {context.points}, want {points}"
public class LoyaltySteps {
private final LoyaltyService service = new LoyaltyService();
private Order order;
private int points;
@Given("a member places an order of ${int}")
public void placesOrder(int amount) { order = new Order(amount); }
@When("the order completes")
public void completes() { points = service.pointsFor(order); }
@Then("the member earns {int} points")
public void earns(int expected) { assertEquals(expected, points); }
}
Task 3: Drive a Feature Outside-In¶
Goal: Implement points_for from scratch using the double loop. Write nothing the failing tests don't demand.
Solution
**The sequence:**1. OUTER red: run scenario → fails (LoyaltyService.points_for missing)
2. INNER: unit test test_base_rate (10 → 10) → implement → green → refactor
3. OUTER: re-run → rows 10 and 49 pass; 50 and 80 fail (no 2x)
4. INNER: unit test test_double_at_50 (50 → 100) → implement threshold → green → refactor
5. OUTER: re-run → all rows green → feature done
Task 4: Fix a UI-Coupled Test (Push to the Service Layer)¶
Goal: A scenario drives the real browser and breaks on every layout change. Move it down to the service layer without changing the business-readable scenario.
Given — brittle UI step definition:
@when('she transfers ${amt:d} from checking to savings')
def step(context, amt):
context.browser.visit("/transfer")
context.browser.fill("#amount", amt)
context.browser.select("#from", "checking")
context.browser.select("#to", "savings")
context.browser.click("#submit") # couples to the DOM; slow; flaky
Solution
The **Gherkin stays exactly the same** (`When she transfers $40 from checking to savings`). Only the step definition changes: ### Python (behave)@when('she transfers ${amt:d} from checking to savings')
def step(context, amt):
context.result = context.bank.transfer( # real service, in-process
user="ada", src="checking", dst="savings", amount=amt)
@then('checking shows ${chk:d} and savings shows ${sav:d}')
def step(context, chk, sav):
assert context.bank.balance("ada", "checking") == chk
assert context.bank.balance("ada", "savings") == sav
Task 5: Make an Imperative Scenario Declarative¶
Goal: Rewrite a click-by-click scenario as a statement of business intent.
Given — imperative:
Scenario: Checkout
Given I am on the home page
When I click "Shop"
And I click "Add to cart" on product "Widget"
And I click the cart icon
And I click "Checkout"
And I fill "card" with "4242..." and click "Pay"
Then I see a page with text "Thank you"
Solution
**Why:** The declarative version describes *what the shopper accomplishes*, not the sequence of clicks. It's readable by the business (living documentation), survives any UI or flow redesign, and pushes all the mechanism (navigation, form filling) down into step definitions. A failure now means "purchase is broken," not "the cart icon moved." The original had no single `When` and asserted on page text — both smells the rewrite fixes.Task 6: Scenario Outline for a Rule Table¶
Goal: Three near-duplicate scenarios share one behavior with different inputs. Collapse them into a Scenario Outline (Specification by Example).
Given — duplicated:
Scenario: Standard user gets no discount
Given a standard user with a $100 cart
When they check out
Then they pay $100
Scenario: Silver user gets 5% off
Given a silver user with a $100 cart
When they check out
Then they pay $95
Scenario: Gold user gets 10% off
Given a gold user with a $100 cart
When they check out
Then they pay $90
Solution
**Why:** One behavior, one scenario, a table of examples. The table reads as the discount-tier specification — the business can review and extend it by adding a row. Each row still runs as a separate test with its own pass/fail, so you keep granular signal while removing duplication.Task 7: Run an Example Mapping Session¶
Goal: Given a story, produce the four card types of Example Mapping (story / rules / examples / questions) — the practical Three Amigos.
Story: "Customers can return items for a refund."
Solution
🟨 STORY: Customer returns an item for a refund
🟦 RULE 1: Returns within 30 days get a full cash refund
🟩 ex: Delivered 5 days ago → full cash refund
🟩 ex: Delivered 30 days ago → full cash refund (boundary)
🟦 RULE 2: Returns after 30 days get store credit only
🟩 ex: Delivered 31 days ago → store credit, no cash (boundary)
🟩 ex: Delivered 90 days ago → store credit
🟦 RULE 3: Final-sale items can't be returned
🟩 ex: Final-sale item, 5 days ago → return rejected
🟥 QUESTION: What about items damaged in shipping after 30 days? (unresolved)
🟥 QUESTION: Is the 30 days from delivery or from purchase? (unresolved)
Task 8: Isolate Scenario Test Data¶
Goal: A suite shares a seeded database; scenarios fail intermittently when run in parallel because they depend on each other's data. Make each scenario own its world.
Given — the problem: scenario B assumes a user that scenario A created.
Solution
### Python (behave) — transactional rollback per scenario# environment.py
def before_scenario(context, scenario):
context.tx = db.begin() # fresh transaction
context.bank = BankService(db)
def after_scenario(context, scenario):
context.tx.rollback() # discard everything the scenario created
Task 9: Stub an External Dependency¶
Goal: A scenario hits a real email/payment provider, making it slow and flaky. Stub the dependency at the boundary; verify behavior against the stub.
Given — calls the real provider:
@when('she requests a password reset')
def step(context):
context.auth.request_reset(context.user) # really sends an email via SES → flaky, slow
Solution
### Python (behave) — inject a fake mailer# environment.py
def before_scenario(context, scenario):
context.mailer = FakeMailer() # records sends, sends nothing
context.auth = AuthService(mailer=context.mailer)
# steps
@when('she requests a password reset')
def step(context):
context.auth.request_reset(context.user)
@then('she receives a reset link by email')
def step(context):
sent = context.mailer.last_to(context.user.email)
assert sent is not None and "reset" in sent.body # assert on behavior, via the fake
Task 10: Rebalance an Ice-Cream-Cone Suite¶
Goal: A team has 200 slow UI scenarios verifying business rules and almost no unit tests. Propose the rebalance and show the move for one rule.
Solution
**The plan:**BEFORE (cone): ~200 UI scenarios · ~10 service · ~20 unit → 35-min flaky build
AFTER (pyramid):
• Move each business RULE from UI → service-layer scenario (fast, stable)
• Keep ~15 UI SMOKE scenarios: prove screens are wired to services
• Build out UNIT tests for edge cases / pure logic (was tested through UI)
→ target: ~15 UI · ~60 service · ~300 unit · <5-min build
# WAS: a UI scenario clicking through the cart to check the total.
# NOW: a service-layer scenario.
Scenario: Gold members get 10% off orders over $100
Given a gold member with a $200 cart
When they check out
Then they are charged $180
Practice Tips¶
- Always see a new scenario fail first — a green test you never watched go red proves nothing.
- Keep
Given/When/Thenin their lanes — setup, one action, observable outcome. - Be declarative — business intent in Gherkin, mechanism in step definitions.
- Default to the service layer; reserve a thin UI layer for wiring smoke tests.
- Use Scenario Outlines for rule tables; include boundary rows.
- Isolate scenario data (transactional rollback / fresh setup) so the suite is parallel-safe and non-flaky.
- Stub external deps at the boundary; verify the real contract separately.
- Do the conversation (Example Mapping) — red cards are the payoff, not the syntax.
← Interview · Craftsmanship Disciplines · Roadmap · Next: Find-Bug
In this topic