Skip to main content

Tutorial: Beautiful Tea

Take a real business requirement and turn it into an executable, living specification. This tutorial walks through the thinking process — from vague requirement to polished spec — and shows the implementation in both SDKs side by side.

Attribution

This tutorial is adapted from Alister Scott's excellent article Specification by Example: a Love Story. It demonstrates how to think in specifications and write tests anyone on your team can understand.


The Business Requirement

You're working for Beautiful Tea, an Australian company that ships tea worldwide. The business hands you this requirement:

"We need to charge different amounts for our customers overseas versus Australia. The tax office says we must charge GST for Australian customers but not for overseas ones. Also, we got a great deal with a local shipping company — we can ship anywhere in Australia for free if they spend more than AUD$100, but we still have to charge overseas customers."

Your mission: implement and document these rules as a living specification.


Step 1: Think in Features, Not Code

❌ The Technical Approach

A developer's first instinct might be:

Feature: Add additional shipping options to shopping cart

This describes what you're building but not why. It focuses on implementation, not business value.

✅ The Business Approach

Rewrite from the business perspective:

Feature: Beautiful Tea Shipping Costs
* Australian customers pay GST
* Overseas customers don't pay GST
* Australian customers get free shipping for orders $100 and above
* Overseas customers all pay the same shipping rate regardless of order size

Notice the difference:

  • No mention of websites, shopping carts, or technical details
  • Focuses on business rules anyone can understand
  • A new team member could read this and understand the shipping policy

Step 2: Think in Scenarios, Not UI Steps

❌ The UI-Focused Approach

A common mistake is writing scenarios that mirror the user interface:

Scenario: Free shipping in Australia
Given I am on the Beautiful Tea home page
When I search for 'Byron Breakfast' tea
Then I see the page for 'Byron Breakfast' tea
When I add 'Byron Breakfast' tea to my cart
And I select 10 as the quantity
Then I see 10 x 'Byron Breakfast' tea in my cart
When I select 'Check Out'
And I enter my country as 'Australia'
Then I see the total including GST
And I see that I am eligible for free shipping

Problems:

  • Very long and repetitive
  • Tied to UI implementation (breaks when UI changes)
  • Doesn't clearly explain the rules
  • You'd need many similar scenarios for different cases

✅ The Declarative Approach

Focus on the business rules using a scenario outline:

Scenario Outline: Calculate GST status and shipping rate
Given the customer is from <country>
When the customer's order totals <order total>
Then the customer pays <GST amount> GST
And they are charged the <shipping rate> shipping rate

Examples:
| country | order total | GST amount | shipping rate |
| Australia | 99.99 | 9.999 | Standard Domestic |
| Australia | 100.00 | 10.00 | Free |
| New Zealand | 99.99 | 0 | Standard International |
| New Zealand | 100.00 | 0 | Standard International |
| Zimbabwe | 100.00 | 0 | Standard International |

Benefits:

  • Crystal clear what the rules are
  • Easy to add new test cases — just add a row
  • Not tied to any UI or implementation
  • Serves as documentation for the entire team
The Gherkin Vocabulary

Steps 1 and 2 used Gherkin vocabulary to express the concepts. The code below shows how each SDK implements this using its own syntax.


Step 3: Build the Domain Code

Before writing the spec, create the business logic. Both SDKs test the same domain:

// src/ShoppingCart.ts

export interface CartItem {
product: string;
quantity: number;
price: number;
}

export class ShoppingCart {
country = '';
items: CartItem[] = [];
gst = 0;
shippingRate = '';

addItem(item: CartItem): void {
this.items.push(item);
}

calculateInvoice(): void {
const total = this.items.reduce(
(sum, item) => sum + item.price * item.quantity, 0
);

// GST: 10% for Australian customers, 0% for everyone else
this.gst = this.country === 'Australia'
? parseFloat((total * 0.1).toFixed(3))
: 0;

// Shipping rules
if (this.country === 'Australia') {
this.shippingRate = total >= 100 ? 'Free' : 'Standard Domestic';
} else {
this.shippingRate = 'Standard International';
}
}
}

Step 4: Write the Feature

Now translate the Gherkin outline from Step 2 into executable code. This is where the SDKs diverge in syntax, but the specification reads the same:

// tests/ShippingCosts.Spec.ts
import {
feature, scenarioOutline, given, when, Then as then, and,
} from '@swedevtools/livedoc-vitest';
import { ShoppingCart } from '../src/ShoppingCart';

feature(`Beautiful Tea Shipping Costs

* Australian customers pay GST
* Overseas customers don't pay GST
* Australian customers get free shipping for orders $100 and above
* Overseas customers all pay the same shipping rate regardless of order size
`, () => {

scenarioOutline(`Calculate GST status and shipping rate

Examples:
| country | order total | GST amount | shipping rate |
| Australia | 99.99 | 9.999 | Standard Domestic |
| Australia | 100.00 | 10.00 | Free |
| New Zealand | 99.99 | 0 | Standard International |
| New Zealand | 100.00 | 0 | Standard International |
| Zimbabwe | 100.00 | 0 | Standard International |
`, (ctx) => {
let cart: ShoppingCart;

given("the customer is from '<country>'", () => {
cart = new ShoppingCart();
cart.country = ctx.example.country;
});

when("the customer's order totals '<order total>'", () => {
cart.addItem({ product: 'tea', quantity: 1, price: ctx.example.orderTotal });
cart.calculateInvoice();
});

then("the customer pays '<GST amount>' GST", () => {
expect(cart.gst).toBe(ctx.example.GSTAmount);
});

and("they are charged the '<shipping rate>' shipping rate", () => {
expect(cart.shippingRate).toBe(ctx.example.shippingRate);
});
});
});

Key TypeScript details:

  • The Examples table is embedded in the scenarioOutline title string
  • Column values are accessed via ctx.example.propertyName (camelCased)
  • Values are automatically type-coerced (strings → numbers, booleans, etc.)
  • Then must be imported uppercase due to ESM thenable detection: Then as then

Step 5: Run It 🎉

npx vitest run ShippingCosts.Spec.ts

Both SDKs produce the same structured output — a readable specification, not just pass/fail:

Feature: Beautiful Tea Shipping Costs
* Australian customers pay GST
* Overseas customers don't pay GST
* Australian customers get free shipping for orders $100 and above
* Overseas customers all pay the same shipping rate regardless of order size

Scenario Outline: Calculate GST status and shipping rate

Example: 1
✓ Given the customer is from 'Australia'
✓ When the customer's order totals '99.99'
✓ Then the customer pays '9.999' GST
✓ And they are charged the 'Standard Domestic' shipping rate

Example: 2
✓ Given the customer is from 'Australia'
✓ When the customer's order totals '100.00'
✓ Then the customer pays '10.00' GST
✓ And they are charged the 'Free' shipping rate

Example: 3
✓ Given the customer is from 'New Zealand'
✓ When the customer's order totals '99.99'
✓ Then the customer pays '0' GST
✓ And they are charged the 'Standard International' shipping rate

...

This output is the specification. Share it with stakeholders, include it in documentation, or view it live in the LiveDoc Viewer.


What Makes This Living Documentation?

Look at what we've achieved:

  1. The spec reads like a business document — A product owner can review the Examples table and ask "what about Express shipping?" without reading code.

  2. The tests ARE the documentation — There's no separate wiki to maintain. When the business rules change, the test changes, and the documentation updates automatically.

  3. Values drive both code and docs — The Examples table is the single source of truth. The test code extracts values from it, so there's zero risk of value drift.

  4. Adding a test case is adding a row — Want to test a new country? Add a row. Want to test a boundary condition? Add a row. The test template handles execution.

  5. Same spec, two SDKs — TypeScript and C# implement the same specification. The reporting model normalizes the output, so the Viewer renders both identically.


Key Takeaways

PrincipleWhat We Did
Think business, not technicalFeature title describes shipping rules, not shopping cart code
Be declarative, not imperativeScenarios describe what should happen, not how through the UI
Use examples liberallyOne outline covers 5 country/amount combinations
Let the spec drive the codeWe wrote the specification first, then the ShoppingCart class
Values in titlesEvery input and expected output is visible in the spec

Try It Yourself

Challenge: Extend the Beautiful Tea spec to handle a new business rule:

"Express shipping is available for Australian orders over $200 at a flat rate of $15. International express is $50 for all countries."

Add rows to the Examples table and update the domain code to match.


Next Steps