- Workshop documentation (WORKSHOP_PLAN, FACILITATOR_GUIDE, etc.) - FizzBuzz kata with demo script (git history to be recreated) - Password Validator kata with demo script and solution - Shopping Cart kata with demo script and solution - Setup guide and TDD reference card for participants
13 KiB
Shopping Cart — Demo Script for Facilitators
Purpose: This script helps you demonstrate the Shopping Cart kata if participants get stuck, or for debriefing discussions. Unlike the FizzBuzz live demo, this is NOT meant to be performed in real-time. Use it as a reference guide.
Target time: 45 minutes (participant hands-on)
Your role: Circulate, answer questions, refer to this script when helping stuck participants
Overview of the Kata
Features to implement (in order):
- Empty cart has subtotal 0 and count 0
- Add items — subtotal = price × quantity
- Remove item by name
- Apply % discount to subtotal
- CartItem rejects zero/negative price & quantity (Value Object)
- Bonus: Duplicate name handling / float precision
Key learning goals:
- Practice RED-GREEN-REFACTOR rhythm
- Let tests drive design decisions (List → Map refactoring)
- Discover Value Object pattern naturally (Step 5)
- Experience design evolution through failing tests
Suggested Test Order & Implementation
STEP 1: Empty Cart (The Baseline)
First test to uncomment (line 23):
test('subtotal is zero when cart is empty', () {
expect(cart.subtotal(), equals(0.0));
});
Expected failure:
- "The method 'subtotal' isn't defined for the class 'ShoppingCart'"
Minimum code to make it GREEN:
class ShoppingCart {
double subtotal() {
return 0.0;
}
}
Next test (line 28):
test('item count is zero when cart is empty', () {
expect(cart.itemCount(), equals(0));
});
Code to add:
int itemCount() {
return 0;
}
Teaching moment: "Always start with the simplest case. Empty state is your baseline."
STEP 2: Adding Items
Test to uncomment (line 39):
test('subtotal reflects a single item added', () {
cart.addItem(CartItem(name: 'Apple', price: 1.50, quantity: 3));
expect(cart.subtotal(), equals(4.50));
});
Expected failure:
- "The class 'CartItem' isn't defined"
- "The method 'addItem' isn't defined"
Minimum code to make it GREEN:
class CartItem {
final String name;
final double price;
final int quantity;
CartItem({required this.name, required this.price, required this.quantity});
}
class ShoppingCart {
final List<CartItem> _items = [];
void addItem(CartItem item) {
_items.add(item);
}
double subtotal() {
if (_items.isEmpty) return 0.0;
return _items.first.price * _items.first.quantity;
}
int itemCount() {
if (_items.isEmpty) return 0;
return _items.first.quantity;
}
}
Teaching moment: "This is intentionally naive! We only handle the first item. The next tests will force us to generalize."
Next test (line 51):
test('subtotal accumulates across multiple different items', () {
cart.addItem(CartItem(name: 'Apple', price: 1.50, quantity: 2)); // 3.00
cart.addItem(CartItem(name: 'Banana', price: 0.75, quantity: 4)); // 3.00
expect(cart.subtotal(), equals(6.00));
});
Expected failure:
- Test fails because
subtotal()only looks at first item
Code to fix:
double subtotal() {
return _items.fold(0.0, (sum, item) => sum + (item.price * item.quantity));
}
int itemCount() {
return _items.fold(0, (sum, item) => sum + item.quantity);
}
Teaching moment: "The test forced us to handle multiple items. We went from naive to general."
STEP 3: Removing Items (The Design Pivot)
Test to uncomment (line 71):
test('removing an item reduces the subtotal', () {
cart.addItem(CartItem(name: 'Apple', price: 1.50, quantity: 2));
cart.addItem(CartItem(name: 'Banana', price: 0.75, quantity: 4));
cart.removeItem('Apple');
expect(cart.subtotal(), equals(3.00));
});
First attempt (works but feels wrong):
void removeItem(String name) {
_items.removeWhere((item) => item.name == name);
}
This passes the tests! But... do we like it?
Teaching moment: "This works, but we're doing an O(n) search every time. The test is telling us something: we're removing by name, not by position. What data structure is optimized for name lookups?"
Refactoring opportunity:
class ShoppingCart {
// Changed from List to Map!
final Map<String, CartItem> _items = {};
void addItem(CartItem item) {
_items[item.name] = item;
}
void removeItem(String name) {
_items.remove(name); // Much simpler!
}
double subtotal() {
return _items.values.fold(0.0, (sum, item) => sum + (item.price * item.quantity));
}
int itemCount() {
return _items.values.fold(0, (sum, item) => sum + item.quantity);
}
}
Run tests → Still GREEN! ✅
Teaching moment: "This is TDD at work:
- The tests gave us confidence to refactor
- The failing test (removeItem) revealed the better design
- Map makes the code simpler AND more efficient
- We discovered this through tests, not upfront planning"
Next test (line 87):
test('removing a non-existent item does nothing', () {
cart.addItem(CartItem(name: 'Apple', price: 1.50, quantity: 2));
cart.removeItem('Orange'); // not in cart
expect(cart.subtotal(), equals(3.00));
});
This already passes! Map.remove() silently handles missing keys.
Teaching moment: "Our refactoring gave us this behavior for free. Good design often does."
STEP 4: Discount Calculation
Test to uncomment (line 109):
test('10% discount reduces subtotal correctly', () {
cart.addItem(CartItem(name: 'Apple', price: 10.00, quantity: 5)); // 50.00
expect(cart.totalAfterDiscount(10), equals(45.00));
});
Expected failure:
- "The method 'totalAfterDiscount' isn't defined"
Minimum code to make it GREEN:
double totalAfterDiscount(double discountPercent) {
final discount = discountPercent / 100;
return subtotal() * (1 - discount);
}
Next tests (lines 115, 121, 127):
- "0% discount returns full subtotal" → passes!
- "100% discount results in zero" → passes!
- "discount on empty cart returns zero" → passes!
Teaching moment: "Notice how we got edge cases for free by implementing the general formula correctly."
STEP 5: Value Object Validation (The Design Pattern)
Test to uncomment (line 139):
test('rejects an item with a zero price', () {
expect(
() => CartItem(name: 'Free thing', price: 0, quantity: 1),
throwsArgumentError,
);
});
First attempt (common mistake):
Many participants will add validation to addItem():
void addItem(CartItem item) {
if (item.price <= 0) {
throw ArgumentError('Price must be greater than zero');
}
_items[item.name] = item;
}
This makes the test pass, BUT...
Ask the participant: "What if CartItem is created somewhere else in the code? Can it exist with a zero price?"
The answer: Yes! And that's a problem.
Better approach - move validation to CartItem constructor:
class CartItem {
final String name;
final double price;
final int quantity;
CartItem({required this.name, required this.price, required this.quantity}) {
if (price <= 0) {
throw ArgumentError('Price must be greater than zero, got: $price');
}
if (quantity <= 0) {
throw ArgumentError('Quantity must be greater than zero, got: $quantity');
}
}
}
Now remove validation from addItem():
void addItem(CartItem item) {
_items[item.name] = item; // Simple again!
}
Run tests → All GREEN! ✅
Teaching moment: "This is the Value Object pattern:
- Immutable data (final fields)
- Validation at construction (in constructor)
- Invalid state is impossible (can't create bad CartItem)
- No need to revalidate (if it exists, it's valid)
Where does validation belong? As close to the data as possible. Not in the cart, but in the item itself."
BONUS: Edge Cases (If Time Permits)
Duplicate names test (line 168):
This test is commented with a design choice - participants must decide:
Option A: Replace (current implementation)
cart.addItem(CartItem(name: 'Apple', price: 1.50, quantity: 2));
cart.addItem(CartItem(name: 'Apple', price: 1.50, quantity: 5));
expect(cart.itemCount(), equals(5)); // replaced, not added
Using _items[item.name] = item naturally replaces duplicates.
Option B: Accumulate
cart.addItem(CartItem(name: 'Apple', price: 1.50, quantity: 2));
cart.addItem(CartItem(name: 'Apple', price: 1.50, quantity: 5));
expect(cart.itemCount(), equals(7)); // quantities stacked
Requires changing addItem():
void addItem(CartItem item) {
if (_items.containsKey(item.name)) {
final existing = _items[item.name]!;
_items[item.name] = CartItem(
name: item.name,
price: item.price,
quantity: existing.quantity + item.quantity,
);
} else {
_items[item.name] = item;
}
}
Teaching moment: "Both are valid! TDD doesn't prescribe the design—you do. The test just documents your decision."
Float precision test (line 181):
test('subtotal handles floating point prices accurately', () {
cart.addItem(CartItem(name: 'Item', price: 0.1, quantity: 3));
expect(cart.subtotal(), closeTo(0.30, 0.001)); // use closeTo!
});
This demonstrates floating-point arithmetic issues (0.1 + 0.1 + 0.1 ≠ 0.3 exactly).
Teaching moment: "Always use closeTo() for float comparisons in tests. This is a fundamental programming lesson!"
Common Participant Questions
"Why use a Map instead of a List?"
Answer: The removeItem(name) test revealed this. With a List, you have to search by name every time (O(n)). With a Map, removal is O(1). The test drove us toward the better data structure.
"Should I validate in CartItem or in ShoppingCart?"
Answer: CartItem. Ask yourself: "Can an invalid CartItem exist?" If yes, you can't trust it anywhere in your code. Value Objects enforce validity at creation time.
"What if I want to accumulate quantities for duplicate names?"
Answer: That's a valid design choice! Write a test that documents that behavior, then implement it. TDD lets you decide—just make the decision explicit through tests.
"Can I add a method to list all items?"
Answer: Do you have a failing test for it? If not, you're writing production code without a test (YAGNI - You Aren't Gonna Need It). Write the test first!
Debrief Discussion Points (5-10 min after kata)
Ask participants:
-
"When did you realize List wasn't the right choice?"
- Most realize during Step 3 (removeItem)
- Some continue with List and never refactor (ask them why!)
-
"What made you switch from List to Map?"
- Listen for: "removeItem by name," "felt inefficient," "Map is cleaner"
- Reinforce: The test drove the design decision
-
"Where did you put the validation initially?"
- Many start in
addItem(), then move toCartItem - Ask: "What changed your mind?"
- Many start in
-
"What's the benefit of validating in the constructor?"
- Invalid state is impossible
- No need to check validity elsewhere
- Confidence that any CartItem is valid
-
"How did the tests help during refactoring?"
- Confidence to change from List to Map
- Proof that behavior didn't break
- Freedom to experiment
-
"What design patterns emerged?"
- Value Object (CartItem validation)
- Collection wrapper (ShoppingCart wrapping Map)
- Functional operations (fold, map, values)
Timing Guidance
For 45-minute practice session:
- 0-5 min: Participants read README, set up files
- 5-15 min: Steps 1-2 (Empty cart, Adding items)
- 15-25 min: Step 3 (Removing items, List → Map refactor)
- 25-30 min: Step 4 (Discount calculation)
- 30-40 min: Step 5 (Value Object validation)
- 40-45 min: Bonus edge cases (if time)
If participants finish early:
- Direct them to bonus tests
- Challenge: "Add a
clear()method using TDD" - Challenge: "Add a
getItems()method that returns a read-only view"
Troubleshooting
Participant stuck on first test
- Check: Did they create
subtotal()method? - Hint: "What's the simplest value you can return?"
Still using List for removeItem
- Ask: "How does removeWhere work internally?"
- Ask: "What if you had 10,000 items?"
- Hint: "What data structure is optimized for name lookups?"
Validation in addItem instead of CartItem
- Ask: "Can a CartItem be created outside of addItem?"
- Ask: "If yes, can it have a negative price?"
- Hint: "Where can you guarantee the item is ALWAYS valid?"
Tests not running
- Check: Did they run
dart pub get? - Check: Are they in the
shopping_cartdirectory? - Check: Did they uncomment the test code?
Float precision issues
- Show:
0.1 + 0.1 + 0.1in Dart REPL - Explain: Floating point representation
- Solution: Use
closeTo(expected, delta)in tests
Summary
This kata demonstrates:
- ✅ Tests driving design decisions (List → Map)
- ✅ Value Object pattern emerging from validation needs
- ✅ Safe refactoring with test coverage
- ✅ Design evolution through incremental testing
- ✅ The power of "make it work, then make it right"
Key facilitator role:
- Encourage the List → Map refactoring (but don't mandate it)
- Guide participants toward constructor validation
- Let them discover design patterns through pain points
- Celebrate when they refactor successfully!
The best learning happens when participants feel the design tension, then resolve it through refactoring.