Initial commit: TDD Workshop exercises for participants
- Password Validator kata with starter code and tests - Shopping Cart kata with starter code and tests - FizzBuzz reference code (from live demo) - Setup guide and TDD reference card - No solutions included (participants implement themselves)
This commit is contained in:
commit
3d94c96ed2
13 changed files with 1469 additions and 0 deletions
62
shopping_cart/README.md
Normal file
62
shopping_cart/README.md
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# Feature B — Shopping Cart
|
||||
|
||||
## Your goal
|
||||
|
||||
Implement `ShoppingCart` using strict TDD. **Do not write any production code before you have a failing test.**
|
||||
|
||||
## Domain rules to implement (in order)
|
||||
|
||||
| Step | Feature | Notes |
|
||||
|------|---------|-------|
|
||||
| 1 | Empty cart has subtotal 0 and count 0 | The baseline |
|
||||
| 2 | Add items — subtotal = price × quantity | Multiple items accumulate |
|
||||
| 3 | Remove item by name | Removing unknown name does nothing |
|
||||
| 4 | Apply % discount to subtotal | 10% off $50 → $45 |
|
||||
| 5 | CartItem rejects zero/negative price & quantity | Value Object validation |
|
||||
| 6 | Bonus: duplicate name / float precision | Design decision — your call |
|
||||
|
||||
## The rhythm — repeat for every rule
|
||||
|
||||
```
|
||||
1. Uncomment the next test → run → watch it FAIL (Red)
|
||||
2. Write the minimum code to make it pass → run → GREEN
|
||||
3. Refactor if needed → run → still GREEN
|
||||
4. Move to the next test
|
||||
```
|
||||
|
||||
## Running the tests
|
||||
|
||||
```bash
|
||||
dart pub get
|
||||
dart test
|
||||
```
|
||||
|
||||
## Files
|
||||
|
||||
- `lib/shopping_cart.dart` — your implementation goes here
|
||||
- `test/shopping_cart_test.dart` — uncomment tests one at a time
|
||||
|
||||
## Design hints (don't peek until you're stuck!)
|
||||
|
||||
<details>
|
||||
<summary>Hint for Step 3 — Remove by name</summary>
|
||||
|
||||
When you need to remove by name, a `List<CartItem>` forces you to search by name.
|
||||
A `Map<String, CartItem>` makes lookup O(1) and removal trivial.
|
||||
Let the test drive you toward the right data structure.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Hint for Step 5 — Value Object</summary>
|
||||
|
||||
Validation belongs in `CartItem`'s constructor, not in `ShoppingCart.addItem()`.
|
||||
If the item can't be created, the cart never sees it.
|
||||
This is the Value Object pattern: invalid state is unrepresentable.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Hint for the float precision bonus test</summary>
|
||||
|
||||
Use `closeTo(expected, delta)` instead of `equals()` for floating point comparisons.
|
||||
Example: `expect(result, closeTo(0.30, 0.001))`
|
||||
</details>
|
||||
46
shopping_cart/lib/shopping_cart.dart
Normal file
46
shopping_cart/lib/shopping_cart.dart
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
// TODO: Implement ShoppingCart using TDD.
|
||||
//
|
||||
// Domain rules (implement one at a time, test first!):
|
||||
// 1. Add an item (name, price, quantity) to the cart
|
||||
// 2. Get the subtotal (sum of price × quantity for all items)
|
||||
// 3. Remove an item by name
|
||||
// 4. Get total item count (sum of all quantities)
|
||||
// 5. Apply a percentage discount to the subtotal (e.g. 10% off)
|
||||
// 6. Items with zero or negative price are rejected
|
||||
//
|
||||
// CartItem is a Value Object — immutable, validated at construction.
|
||||
// ShoppingCart is stateful — it accumulates items across calls.
|
||||
|
||||
class CartItem {
|
||||
final String name;
|
||||
final double price;
|
||||
final int quantity;
|
||||
|
||||
CartItem({required this.name, required this.price, required this.quantity}) {
|
||||
// TODO: validate price > 0 and quantity > 0
|
||||
}
|
||||
}
|
||||
|
||||
class ShoppingCart {
|
||||
// TODO: implement
|
||||
|
||||
void addItem(CartItem item) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
void removeItem(String name) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
double subtotal() {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
int itemCount() {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
double totalAfterDiscount(double discountPercent) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
8
shopping_cart/pubspec.yaml
Normal file
8
shopping_cart/pubspec.yaml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
name: shopping_cart
|
||||
description: TDD Workshop - Feature B
|
||||
|
||||
environment:
|
||||
sdk: ^3.0.0
|
||||
|
||||
dev_dependencies:
|
||||
test: ^1.25.0
|
||||
187
shopping_cart/test/shopping_cart_test.dart
Normal file
187
shopping_cart/test/shopping_cart_test.dart
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
import 'package:test/test.dart';
|
||||
import '../lib/shopping_cart.dart';
|
||||
|
||||
// ============================================================
|
||||
// SHOPPING CART — Workshop Starter
|
||||
// Follow Red → Green → Refactor strictly.
|
||||
// Uncomment one test at a time. Make it pass. Then the next.
|
||||
// ============================================================
|
||||
|
||||
void main() {
|
||||
late ShoppingCart cart;
|
||||
|
||||
setUp(() {
|
||||
cart = ShoppingCart();
|
||||
});
|
||||
|
||||
// ──────────────────────────────────────────────────────────
|
||||
// STEP 1 — Empty cart
|
||||
// Before adding anything, the cart should be empty.
|
||||
// Trivial, but sets the baseline. Always start here.
|
||||
// ──────────────────────────────────────────────────────────
|
||||
group('Empty cart', () {
|
||||
test('subtotal is zero when cart is empty', () {
|
||||
// TODO: uncomment when ready
|
||||
// expect(cart.subtotal(), equals(0.0));
|
||||
});
|
||||
|
||||
test('item count is zero when cart is empty', () {
|
||||
// TODO: uncomment when ready
|
||||
// expect(cart.itemCount(), equals(0));
|
||||
});
|
||||
});
|
||||
|
||||
// ──────────────────────────────────────────────────────────
|
||||
// STEP 2 — Adding items
|
||||
// A single item: subtotal = price × quantity.
|
||||
// ──────────────────────────────────────────────────────────
|
||||
group('Adding items', () {
|
||||
test('subtotal reflects a single item added', () {
|
||||
// TODO: uncomment when ready
|
||||
// cart.addItem(CartItem(name: 'Apple', price: 1.50, quantity: 3));
|
||||
// expect(cart.subtotal(), equals(4.50));
|
||||
});
|
||||
|
||||
test('item count reflects quantity of a single item', () {
|
||||
// TODO: uncomment when ready
|
||||
// cart.addItem(CartItem(name: 'Apple', price: 1.50, quantity: 3));
|
||||
// expect(cart.itemCount(), equals(3));
|
||||
});
|
||||
|
||||
test('subtotal accumulates across multiple different items', () {
|
||||
// TODO: uncomment when ready
|
||||
// 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));
|
||||
});
|
||||
|
||||
test('item count sums quantities across multiple items', () {
|
||||
// TODO: uncomment when ready
|
||||
// cart.addItem(CartItem(name: 'Apple', price: 1.50, quantity: 2));
|
||||
// cart.addItem(CartItem(name: 'Banana', price: 0.75, quantity: 4));
|
||||
// expect(cart.itemCount(), equals(6));
|
||||
});
|
||||
});
|
||||
|
||||
// ──────────────────────────────────────────────────────────
|
||||
// STEP 3 — Removing items
|
||||
// Removing an item by name should drop it from the subtotal.
|
||||
// ──────────────────────────────────────────────────────────
|
||||
group('Removing items', () {
|
||||
test('removing an item reduces the subtotal', () {
|
||||
// TODO: uncomment when ready
|
||||
// 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));
|
||||
});
|
||||
|
||||
test('removing an item reduces the item count', () {
|
||||
// TODO: uncomment when ready
|
||||
// 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.itemCount(), equals(4));
|
||||
});
|
||||
|
||||
test('removing a non-existent item does nothing', () {
|
||||
// TODO: uncomment when ready
|
||||
// cart.addItem(CartItem(name: 'Apple', price: 1.50, quantity: 2));
|
||||
// cart.removeItem('Orange'); // not in cart
|
||||
// expect(cart.subtotal(), equals(3.00));
|
||||
});
|
||||
|
||||
test('cart is empty after removing the only item', () {
|
||||
// TODO: uncomment when ready
|
||||
// cart.addItem(CartItem(name: 'Apple', price: 1.50, quantity: 2));
|
||||
// cart.removeItem('Apple');
|
||||
// expect(cart.subtotal(), equals(0.0));
|
||||
// expect(cart.itemCount(), equals(0));
|
||||
});
|
||||
});
|
||||
|
||||
// ──────────────────────────────────────────────────────────
|
||||
// STEP 4 — Discount
|
||||
// A percentage discount reduces the subtotal proportionally.
|
||||
// Think: what is 10% off $50.00? → $45.00
|
||||
// ──────────────────────────────────────────────────────────
|
||||
group('Applying a percentage discount', () {
|
||||
test('10% discount reduces subtotal correctly', () {
|
||||
// TODO: uncomment when ready
|
||||
// cart.addItem(CartItem(name: 'Apple', price: 10.00, quantity: 5)); // 50.00
|
||||
// expect(cart.totalAfterDiscount(10), equals(45.00));
|
||||
});
|
||||
|
||||
test('0% discount returns the full subtotal', () {
|
||||
// TODO: uncomment when ready
|
||||
// cart.addItem(CartItem(name: 'Apple', price: 10.00, quantity: 5));
|
||||
// expect(cart.totalAfterDiscount(0), equals(50.00));
|
||||
});
|
||||
|
||||
test('100% discount results in zero total', () {
|
||||
// TODO: uncomment when ready
|
||||
// cart.addItem(CartItem(name: 'Apple', price: 10.00, quantity: 5));
|
||||
// expect(cart.totalAfterDiscount(100), equals(0.0));
|
||||
});
|
||||
|
||||
test('discount on empty cart returns zero', () {
|
||||
// TODO: uncomment when ready
|
||||
// expect(cart.totalAfterDiscount(20), equals(0.0));
|
||||
});
|
||||
});
|
||||
|
||||
// ──────────────────────────────────────────────────────────
|
||||
// STEP 5 — CartItem validation (Value Object)
|
||||
// Invalid items should be rejected at construction time.
|
||||
// This is the "Value Object" pattern: impossible to create bad state.
|
||||
// ──────────────────────────────────────────────────────────
|
||||
group('CartItem validation', () {
|
||||
test('rejects an item with a zero price', () {
|
||||
// TODO: uncomment when ready
|
||||
// expect(
|
||||
// () => CartItem(name: 'Free thing', price: 0, quantity: 1),
|
||||
// throwsArgumentError,
|
||||
// );
|
||||
});
|
||||
|
||||
test('rejects an item with a negative price', () {
|
||||
// TODO: uncomment when ready
|
||||
// expect(
|
||||
// () => CartItem(name: 'Broken item', price: -5.00, quantity: 1),
|
||||
// throwsArgumentError,
|
||||
// );
|
||||
});
|
||||
|
||||
test('rejects an item with zero quantity', () {
|
||||
// TODO: uncomment when ready
|
||||
// expect(
|
||||
// () => CartItem(name: 'Apple', price: 1.50, quantity: 0),
|
||||
// throwsArgumentError,
|
||||
// );
|
||||
});
|
||||
});
|
||||
|
||||
// ──────────────────────────────────────────────────────────
|
||||
// BONUS — Edge cases (if you finish early)
|
||||
// ──────────────────────────────────────────────────────────
|
||||
group('Edge cases', () {
|
||||
test('adding the same item name twice replaces it (or accumulates — you decide!)', () {
|
||||
// TODO: uncomment and decide the behavior, then implement it
|
||||
// Hint: this is a design decision. TDD lets the test define the rule.
|
||||
//
|
||||
// Option A — replace:
|
||||
// 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
|
||||
//
|
||||
// Option B — accumulate:
|
||||
// expect(cart.itemCount(), equals(7)); // stacked
|
||||
});
|
||||
|
||||
test('subtotal handles floating point prices accurately', () {
|
||||
// TODO: uncomment when ready
|
||||
// cart.addItem(CartItem(name: 'Item', price: 0.1, quantity: 3)); // 0.1 + 0.1 + 0.1
|
||||
// expect(cart.subtotal(), closeTo(0.30, 0.001)); // use closeTo for floats!
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue