tdd-workshop-exercises/shopping_cart/test/shopping_cart_test.dart
fiatcode 3d94c96ed2 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)
2026-03-10 15:37:58 +07:00

187 lines
8.4 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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