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
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