- 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
187 lines
8.4 KiB
Dart
187 lines
8.4 KiB
Dart
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!
|
||
});
|
||
});
|
||
}
|