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:
fiatcode 2026-03-10 15:37:58 +07:00
commit 3d94c96ed2
13 changed files with 1469 additions and 0 deletions

View 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!
});
});
}