From 3d94c96ed2834933b0019173787504aab44b14a2 Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Mar 2026 15:37:58 +0700 Subject: [PATCH] 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) --- .gitignore | 22 + README.md | 200 ++++++++ SETUP_GUIDE.md | 434 ++++++++++++++++++ TDD_REFERENCE_CARD.md | 276 +++++++++++ fizzbuzz | 1 + password_validator/README.md | 43 ++ .../lib/password_validator.dart | 25 + password_validator/pubspec.yaml | 8 + .../test/password_validator_test.dart | 157 +++++++ shopping_cart/README.md | 62 +++ shopping_cart/lib/shopping_cart.dart | 46 ++ shopping_cart/pubspec.yaml | 8 + shopping_cart/test/shopping_cart_test.dart | 187 ++++++++ 13 files changed, 1469 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 SETUP_GUIDE.md create mode 100644 TDD_REFERENCE_CARD.md create mode 160000 fizzbuzz create mode 100644 password_validator/README.md create mode 100644 password_validator/lib/password_validator.dart create mode 100644 password_validator/pubspec.yaml create mode 100644 password_validator/test/password_validator_test.dart create mode 100644 shopping_cart/README.md create mode 100644 shopping_cart/lib/shopping_cart.dart create mode 100644 shopping_cart/pubspec.yaml create mode 100644 shopping_cart/test/shopping_cart_test.dart diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9cc791a --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# Dart build artifacts +.dart_tool/ +.packages +pubspec.lock +build/ + +# IDE +.idea/ +.vscode/ +*.iml + +# OS +.DS_Store +Thumbs.db + +# Temporary files +*.log +*.tmp + +# FizzBuzz nested git repository (keep the .git for demo purposes) +# The fizzbuzz/.git directory contains TDD progression commits +# It's intentionally kept separate from the main workshop repo diff --git a/README.md b/README.md new file mode 100644 index 0000000..fe84257 --- /dev/null +++ b/README.md @@ -0,0 +1,200 @@ +# TDD Workshop - Exercises + +**A hands-on, 2-hour workshop for learning Test-Driven Development through deliberate practice.** + +This repository contains the exercise materials for the TDD workshop. Follow the setup guide to prepare before the workshop. + +--- + +## Before the Workshop + +### 1. Complete Setup + +Follow the **[SETUP_GUIDE.md](SETUP_GUIDE.md)** to: +- Install Dart SDK +- Verify your development environment +- Run a test to confirm everything works + +### 2. Verify Installation + +Test that everything is working: + +```bash +cd password_validator +dart pub get +dart test # Should see "All tests skipped" or similar +``` + +### 3. Bookmark Reference + +Keep **[TDD_REFERENCE_CARD.md](TDD_REFERENCE_CARD.md)** handy during the workshop for quick reference on the RED-GREEN-REFACTOR cycle. + +--- + +## Repository Structure + +``` +tdd-workshop-exercises/ +├── README.md (you are here) +├── SETUP_GUIDE.md # Setup instructions +├── TDD_REFERENCE_CARD.md # Quick reference for TDD +│ +├── fizzbuzz/ # Live demo kata (reference) +│ ├── README.md +│ ├── lib/fizzbuzz.dart +│ └── test/fizzbuzz_test.dart +│ +├── password_validator/ # Hands-on exercise (Option A) +│ ├── README.md # Exercise instructions +│ ├── lib/password_validator.dart # TODO: Implement here +│ └── test/password_validator_test.dart # Uncomment tests step-by-step +│ +└── shopping_cart/ # Hands-on exercise (Option B) + ├── README.md # Exercise instructions + ├── lib/shopping_cart.dart # TODO: Implement here + └── test/shopping_cart_test.dart # Uncomment tests step-by-step +``` + +--- + +## During the Workshop + +### Part 1: Live Demo (15 min) + +Watch the facilitator demonstrate the RED-GREEN-REFACTOR cycle with **FizzBuzz**. + +The `fizzbuzz/` directory contains reference code you can review later. + +### Part 2: Choose Your Kata (45 min) + +Pick **one** kata to practice: + +#### Option A: Password Validator +- Build a password validator with 5 rules +- Practice rule-based validation +- Discover the Open-Closed Principle through refactoring + +**Start here:** `password_validator/README.md` + +#### Option B: Shopping Cart +- Build a shopping cart with add, remove, discount +- Practice stateful domain modeling +- Discover when to use Map vs List +- Learn the Value Object pattern + +**Start here:** `shopping_cart/README.md` + +### Part 3: Debrief & Reflection (10 min) + +Share learnings and discuss design insights with the group. + +--- + +## The TDD Workflow + +For each kata, follow this rhythm: + +``` +1. Uncomment the next test +2. Run tests → watch it FAIL (RED) +3. Write minimal code to make it pass (GREEN) +4. Refactor if needed → tests stay GREEN +5. Commit your work +6. Repeat with the next test +``` + +**Key principles:** +- Write the test BEFORE the implementation +- Take small steps (one test at a time) +- Make it work first, make it clean second +- Let the tests drive your design + +--- + +## Running the Katas + +### Password Validator +```bash +cd password_validator +dart pub get +dart test +``` + +### Shopping Cart +```bash +cd shopping_cart +dart pub get +dart test +``` + +### FizzBuzz (reference) +```bash +cd fizzbuzz +dart pub get +dart test +``` + +--- + +## Troubleshooting + +Having issues? See **[SETUP_GUIDE.md](SETUP_GUIDE.md)** for: +- Dart installation problems +- `dart pub get` failures +- Test execution errors +- IDE configuration + +--- + +## After the Workshop + +### Continue Practicing + +Explore more katas at: +- **TDD Katas Collection:** https://github.com/dhemasnurjaya/tdd-katas +- **Kata-Log:** https://kata-log.rocks +- **Exercism:** https://exercism.org +- **Coding Dojo:** https://codingdojo.org + +### Recommended Reading + +- *Test-Driven Development* by Kent Beck +- *Clean Code* by Robert C. Martin +- *Growing Object-Oriented Software, Guided by Tests* by Freeman & Pryce + +### Keep Practicing! + +The **RED-GREEN-REFACTOR** cycle becomes second nature with practice: +1. Start with small, obvious katas +2. Practice the rhythm until it's automatic +3. Gradually tackle more complex problems +4. Focus on letting tests drive design decisions + +--- + +## Questions? + +During the workshop: +- Raise your hand or ask your facilitator +- Don't hesitate to ask for help if you're stuck! + +Before the workshop: +- Contact your workshop facilitator +- Verify your setup using SETUP_GUIDE.md + +--- + +## Philosophy + +> "Clean code that works." — Ron Jeffries + +TDD is a **discipline**, not a burden: +- Tests first (even for "obvious" code) +- Small steps (one test at a time) +- Refactor fearlessly (tests protect you) + +**The goal:** Build muscle memory for the RED-GREEN-REFACTOR cycle. + +--- + +**Ready to practice? See [SETUP_GUIDE.md](SETUP_GUIDE.md) to get started!** diff --git a/SETUP_GUIDE.md b/SETUP_GUIDE.md new file mode 100644 index 0000000..5fd6da2 --- /dev/null +++ b/SETUP_GUIDE.md @@ -0,0 +1,434 @@ +# Workshop Setup Guide + +**Complete these steps BEFORE the workshop to ensure smooth experience.** + +--- + +## Prerequisites + +### Required Software + +- **Dart SDK** version 3.0.0 or higher +- **Git** (for cloning the repository) +- **A code editor** (VS Code, IntelliJ IDEA, or any editor you prefer) + +### Recommended (Optional) + +- **VS Code Extensions**: + - Dart (official) + - Flutter (if working with Flutter projects) +- **IntelliJ/Android Studio Plugins**: + - Dart plugin + - Flutter plugin (if applicable) + +--- + +## Installation Steps + +### 1. Install Dart SDK + +#### macOS (using Homebrew) +```bash +brew tap dart-lang/dart +brew install dart +``` + +#### Linux (using apt) +```bash +sudo apt-get update +sudo apt-get install dart +``` + +#### Windows (using Chocolatey) +```bash +choco install dart-sdk +``` + +#### All Platforms (Manual Download) +Download from: https://dart.dev/get-dart + +### 2. Verify Dart Installation + +```bash +dart --version +``` + +**Expected output:** Something like `Dart SDK version: 3.x.x` + +If you see an error, Dart isn't in your PATH. See Troubleshooting section below. + +### 3. Clone the Workshop Repository + +```bash +git clone +cd tdd-workshop +``` + +*(Your facilitator will provide the actual repository URL)* + +### 4. Set Up Each Kata + +Navigate to each kata directory and install dependencies: + +#### Password Validator +```bash +cd password_validator +dart pub get +cd .. +``` + +**Expected output:** `Got dependencies!` or similar success message + +#### Shopping Cart +```bash +cd shopping_cart +dart pub get +cd .. +``` + +#### FizzBuzz (Demo) +```bash +cd fizzbuzz +dart pub get +cd .. +``` + +--- + +## Verification + +Let's make sure everything works! + +### Test 1: Run Password Validator Tests + +```bash +cd password_validator +dart test +``` + +**Expected output:** +``` +All tests skipped. +``` +OR +``` +00:00 +0: All tests passed! +``` + +If you see errors, see Troubleshooting section. + +### Test 2: Run Shopping Cart Tests + +```bash +cd shopping_cart +dart test +``` + +**Expected output:** Same as above (all tests skipped or passing) + +### Test 3: Run FizzBuzz Tests + +```bash +cd fizzbuzz +dart test +``` + +**Expected output:** +``` +00:00 +5: All tests passed! +``` +*(FizzBuzz has completed tests)* + +--- + +## IDE Setup (Optional but Recommended) + +### Visual Studio Code + +1. Install VS Code: https://code.visualstudio.com/ +2. Install Dart extension: + - Open VS Code + - Go to Extensions (Ctrl+Shift+X / Cmd+Shift+X) + - Search for "Dart" + - Install the official Dart extension + +3. Open the workshop folder: + ```bash + code tdd-workshop + ``` + +4. Verify syntax highlighting works: + - Open `password_validator/lib/password_validator.dart` + - Code should be colorized + - You should see IntelliSense when typing + +### IntelliJ IDEA / Android Studio + +1. Install IntelliJ IDEA (Community Edition is free): https://www.jetbrains.com/idea/ +2. Install Dart plugin: + - Go to Settings → Plugins + - Search for "Dart" + - Install and restart + +3. Open the workshop folder: + - File → Open → Navigate to `tdd-workshop` + +--- + +## Day-of-Workshop Checklist + +**Have these ready BEFORE the workshop starts:** + +- [ ] Laptop charged (or bring charger) +- [ ] Workshop repository cloned and dependencies installed +- [ ] All verification tests passing +- [ ] Code editor open with workshop directory loaded +- [ ] Terminal window ready +- [ ] This reference card bookmarked: `TDD_REFERENCE_CARD.md` + +**Optional nice-to-haves:** +- [ ] Two monitor setup (or large screen for split view) +- [ ] Comfortable keyboard +- [ ] Water/coffee nearby + +--- + +## Troubleshooting + +### Problem: `dart: command not found` + +**Cause:** Dart isn't in your system PATH + +**Fix (macOS/Linux):** +1. Find where Dart is installed: + ```bash + which dart + ``` + +2. If nothing shows up, add Dart to PATH: + ```bash + # For Homebrew installation on macOS + echo 'export PATH="$PATH:/usr/local/opt/dart/libexec"' >> ~/.zshrc + source ~/.zshrc + + # For manual installation + echo 'export PATH="$PATH:/path/to/dart-sdk/bin"' >> ~/.zshrc + source ~/.zshrc + ``` + +3. Verify: + ```bash + dart --version + ``` + +**Fix (Windows):** +1. Search for "Environment Variables" in Start menu +2. Click "Edit system environment variables" +3. Click "Environment Variables" +4. Find "Path" in System variables +5. Add the path to dart-sdk/bin (e.g., `C:\tools\dart-sdk\bin`) +6. Restart terminal and verify: `dart --version` + +--- + +### Problem: `dart pub get` fails with network error + +**Cause:** Firewall, proxy, or network restrictions + +**Fix:** +1. Check internet connection +2. If behind corporate proxy: + ```bash + # Set proxy (replace with your proxy details) + export HTTP_PROXY=http://proxy.company.com:8080 + export HTTPS_PROXY=http://proxy.company.com:8080 + dart pub get + ``` + +3. If still failing, try: + ```bash + dart pub get --verbose + ``` + *(Shows detailed error messages)* + +--- + +### Problem: `dart pub get` succeeds but `dart test` fails with package errors + +**Cause:** Corrupted package cache + +**Fix:** +```bash +# Clear pub cache and reinstall +dart pub cache repair +cd password_validator +dart pub get +dart test +``` + +--- + +### Problem: Tests run but show errors like "Target of URI doesn't exist" + +**Cause:** Dependencies not installed or IDE not recognizing them + +**Fix:** +1. Run `dart pub get` again in that directory +2. Restart your IDE +3. If using VS Code, run command: "Dart: Restart Analysis Server" + - Ctrl+Shift+P (Cmd+Shift+P on Mac) → Search for "Dart: Restart" + +--- + +### Problem: VS Code doesn't recognize Dart files + +**Cause:** Dart extension not installed or not enabled + +**Fix:** +1. Open Extensions (Ctrl+Shift+X) +2. Search "Dart" +3. Make sure official Dart extension is installed and enabled +4. Restart VS Code + +--- + +### Problem: Tests are running but output is hard to read + +**Fix:** +```bash +# Run tests with verbose output +dart test --reporter=expanded + +# Run specific test file +dart test test/password_validator_test.dart +``` + +--- + +### Problem: Getting "version solving failed" errors + +**Cause:** Dart SDK version mismatch + +**Fix:** +1. Check your Dart version: + ```bash + dart --version + ``` + +2. If version is below 3.0.0, update Dart: + ```bash + # macOS + brew upgrade dart + + # Linux + sudo apt-get update + sudo apt-get install dart + + # Windows + choco upgrade dart-sdk + ``` + +3. Re-run: + ```bash + dart pub get + ``` + +--- + +### Problem: Can't clone repository (Git not installed) + +**Fix:** +1. Install Git: + - **macOS**: `brew install git` + - **Linux**: `sudo apt-get install git` + - **Windows**: Download from https://git-scm.com/ + +2. Verify: + ```bash + git --version + ``` + +3. Try cloning again + +--- + +### Still Having Issues? + +**During the workshop:** +- Arrive 10 minutes early and ask the facilitator for help +- Bring this troubleshooting guide with you + +**Before the workshop:** +- Email the facilitator with: + - Output of `dart --version` + - Full error message from the failing command + - Your operating system + +--- + +## Workspace Layout Suggestions + +### Option 1: Split Screen (Recommended for Beginners) +``` +┌──────────────────┬──────────────────┐ +│ │ │ +│ Editor │ Editor │ +│ (lib/ files) │ (test/ files) │ +│ │ │ +│ │ │ +├──────────────────┴──────────────────┤ +│ Terminal (dart test) │ +└─────────────────────────────────────┘ +``` + +### Option 2: Two Monitors +``` +Monitor 1 Monitor 2 +┌──────────────┐ ┌──────────────┐ +│ │ │ │ +│ Editor │ │ Browser │ +│ (Full) │ │ - Workshop │ +│ │ │ guide │ +│ │ │ - Reference │ +│ │ │ card │ +└──────────────┘ └──────────────┘ +``` + +### Option 3: Single Screen (Compact) +``` +┌─────────────────────────────────────┐ +│ Editor (maximized) │ +│ Tabs: lib file | test file │ +│ │ +│ │ +│ │ +└─────────────────────────────────────┘ +Use Alt+Tab to switch to terminal +``` + +--- + +## Quick Command Reference + +| Task | Command | +|------|---------| +| Run all tests | `dart test` | +| Run specific test file | `dart test test/password_validator_test.dart` | +| Run tests with detailed output | `dart test --reporter=expanded` | +| Watch mode (re-run on changes) | `dart test --watch` *(experimental)* | +| Install dependencies | `dart pub get` | +| Check Dart version | `dart --version` | +| Clear pub cache | `dart pub cache repair` | + +--- + +## Support During Workshop + +If you have issues during the workshop: +1. **Raise your hand** — facilitator will come to you +2. **Ask a neighbor** — pair programming is encouraged +3. **Check this guide** — most issues are covered here + +--- + +**You're all set!** See you at the workshop. 🚀 diff --git a/TDD_REFERENCE_CARD.md b/TDD_REFERENCE_CARD.md new file mode 100644 index 0000000..a1a6bcc --- /dev/null +++ b/TDD_REFERENCE_CARD.md @@ -0,0 +1,276 @@ +# TDD Reference Card + +**Quick reference for Test-Driven Development practice** + +--- + +## The RED-GREEN-REFACTOR Cycle + +``` +┌─────────────────────────────────────────────────┐ +│ │ +│ 1. RED Write a failing test │ +│ ↓ (Proves the test can fail) │ +│ │ +│ 2. GREEN Write minimal code to pass │ +│ ↓ (Make it work, don't make it perfect)│ +│ │ +│ 3. REFACTOR Clean up code │ +│ ↓ (Remove duplication, improve names)│ +│ │ +│ 4. REPEAT Next failing test │ +│ ↑ │ +│ └───────────────────────────────────────────┘ +``` + +--- + +## The Discipline + +### RED - Write a Failing Test +- **Write the test first**, before any production code +- Run it and **watch it fail** (RED ❌) +- Make sure it fails **for the right reason** +- If it passes immediately, the test isn't testing anything new + +### GREEN - Make It Pass +- Write the **simplest code** that makes the test pass +- Don't worry about perfection yet +- **Hardcoding is okay** if it passes the test +- Run the test and see it **pass** (GREEN ✅) +- If you're tempted to write "just one more line"—STOP and run the test + +### REFACTOR - Clean Up +- Now that tests are passing, **improve the code** +- Remove duplication +- Clarify names +- Extract methods or classes +- **Run tests after every change** to ensure they stay green +- If tests go RED during refactoring, undo and try smaller steps + +--- + +## When to Refactor + +Look for these **code smells**: + +| Smell | Refactoring | +|-------|-------------| +| **Duplication** | Extract method, introduce constant | +| **Unclear names** | Rename variable/method/class | +| **Long method** | Extract smaller methods | +| **Nested conditionals** | Extract methods, introduce polymorphism | +| **Magic numbers** | Replace with named constants | +| **God class** | Split responsibilities into multiple classes | + +**Rule of thumb:** If you have to write a comment explaining what code does, the code needs better names. + +--- + +## Common Test Patterns + +### Arrange-Act-Assert (AAA) + +```dart +test('calculates total price correctly', () { + // ARRANGE - Set up test data + final cart = ShoppingCart(); + cart.addItem(CartItem(name: 'Apple', price: 1.50, quantity: 3)); + + // ACT - Perform the action being tested + final total = cart.subtotal(); + + // ASSERT - Verify the result + expect(total, equals(4.50)); +}); +``` + +### setUp() for Common Setup + +```dart +void main() { + late Calculator calculator; + + setUp(() { + calculator = Calculator(); // Runs before EACH test + }); + + test('adds two numbers', () { + expect(calculator.add(2, 3), equals(5)); + }); +} +``` + +### Test Naming + +Use descriptive names that explain **what** is being tested: + +✅ **Good**: `test('rejects password shorter than 8 characters', ...)` +❌ **Bad**: `test('test1', ...)` or `test('validation works', ...)` + +--- + +## Dart Test Assertions - Quick Reference + +```dart +// Equality +expect(actual, equals(expected)); +expect(result, isTrue); +expect(result, isFalse); + +// Comparison +expect(value, greaterThan(10)); +expect(value, lessThan(5)); +expect(value, closeTo(0.30, 0.001)); // For floating point + +// Type checks +expect(object, isA()); +expect(value, isNull); +expect(value, isNotNull); + +// Collections +expect(list, isEmpty); +expect(list, isNotEmpty); +expect(list, contains('item')); +expect(list, containsAll(['a', 'b'])); +expect(list, hasLength(3)); + +// Exceptions +expect(() => validatePassword(''), throwsArgumentError); +expect(() => divide(1, 0), throwsA(isA())); + +// Strings +expect(text, startsWith('Hello')); +expect(text, endsWith('world')); +expect(text, matches(RegExp(r'\d+'))); +``` + +--- + +## Quick Tips for TDD Success + +### 1. Start Small +Begin with the **simplest possible test**: +- Empty input → sensible output +- Single item → correct behavior +- Then add complexity + +### 2. One Test at a Time +- Uncomment or write **one test** +- Make it pass +- Then move to the next +- **Don't skip ahead!** + +### 3. Baby Steps +Each cycle should take **2-5 minutes**, not 20 minutes: +- If a test takes too long, it's testing too much +- Break it into smaller tests + +### 4. Run Tests Frequently +After **every code change**: +- Immediately see if something broke +- Faster feedback = faster learning + +### 5. Trust the Process +The design **emerges** from the tests: +- You don't need to design everything upfront +- Simple tests lead to general solutions +- Complex scenarios often work without explicit implementation + +### 6. Refactor Fearlessly +With passing tests, you can: +- Rename aggressively +- Restructure boldly +- Optimize confidently +- If tests go RED, just undo + +--- + +## Common Mistakes to Avoid + +| Mistake | Why It's Bad | Fix | +|---------|--------------|-----| +| Writing code before test | No way to know if test is valid | Always RED first | +| Writing multiple tests at once | Overwhelming, loses focus | One test at a time | +| Skipping REFACTOR | Technical debt accumulates | Clean up after each GREEN | +| Not running tests after refactor | Might break something unknowingly | Run tests constantly | +| Testing implementation details | Tests become brittle | Test behavior, not internals | +| Making tests pass by hardcoding | Doesn't prove general solution | Add another test to force generalization | + +--- + +## What to TDD vs. What to Skip + +### ✅ Great for TDD: +- **Business logic**: Calculations, validations, algorithms +- **Domain models**: Value objects, entities with rules +- **Utilities**: String parsers, formatters, converters +- **APIs**: Request/response handling, error cases + +### ⚠️ Less Useful for TDD: +- **UI layouts**: Visual appearance is hard to test +- **Framework glue**: Wiring dependencies, configuration +- **Database queries**: Better tested with integration tests +- **Third-party integrations**: Better mocked or integration tested + +**Rule:** If behavior matters and can be verified programmatically, use TDD. + +--- + +## When You Get Stuck + +### "I don't know what test to write next" +Ask: **"What's the simplest behavior this doesn't handle yet?"** +- Start with zero/empty cases +- Then one item +- Then two items +- Then edge cases + +### "The test is too complex" +**Break it down:** +- Can you test just one part of the behavior? +- Can you introduce a helper method to simplify setup? + +### "I can't make it pass without writing a lot of code" +**That's a sign:** +- The test might be too big (break into smaller tests) +- OR you're missing an intermediate test +- OR you need to refactor existing code first + +### "Tests are passing but the design feels wrong" +**Good news:** +- You have tests as a safety net! +- **Refactor now** while tests are green +- Improve names, extract methods, restructure + +--- + +## Resources for More Practice + +### After This Workshop: +- **tdd-katas repo**: github.com/dhemasnurjaya/tdd-katas + - 5 complete katas with commit history + - Roman Numerals, Bowling Game, Gilded Rose, String Calculator, Mars Rover + +### Books: +- *Test-Driven Development* by Kent Beck +- *Clean Code* by Robert C. Martin +- *Growing Object-Oriented Software, Guided by Tests* by Freeman & Pryce + +### Online Katas: +- Kata-Log (kata-log.rocks) +- Coding Dojo (codingdojo.org) +- Exercism (exercism.org) + +--- + +## Remember + +> "Clean code that works." — Ron Jeffries + +TDD is a **skill**. Like any skill: +- It feels awkward at first +- Practice makes it automatic +- The rhythm becomes second nature + +**Keep practicing. Keep the cycle tight. Let the tests guide you.** diff --git a/fizzbuzz b/fizzbuzz new file mode 160000 index 0000000..51ab515 --- /dev/null +++ b/fizzbuzz @@ -0,0 +1 @@ +Subproject commit 51ab5159f4c8407cd47289c8e06b401b9774b821 diff --git a/password_validator/README.md b/password_validator/README.md new file mode 100644 index 0000000..12c5504 --- /dev/null +++ b/password_validator/README.md @@ -0,0 +1,43 @@ +# Feature A — Password Validator + +## Your goal + +Implement `PasswordValidator` using strict TDD. **Do not write any production code before you have a failing test.** + +## Rules to implement (in order) + +| Step | Rule | Error message | +|------|------|---------------| +| 1 | At least 8 characters long | `Must be at least 8 characters long` | +| 2 | At least one uppercase letter (A–Z) | `Must contain at least one uppercase letter` | +| 3 | At least one digit (0–9) | `Must contain at least one digit` | +| 4 | At least one special character (`!@#$%^&*`) | `Must contain at least one special character (!@#$%^&*)` | +| 5 | No spaces | `Must not contain spaces` | +| 6 | All errors reported at once (refactor) | — | + +## 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/password_validator.dart` — your implementation goes here +- `test/password_validator_test.dart` — uncomment tests one at a time + +## Hint for Step 6 (the refactor insight) + +Once all 5 rules pass, you'll likely have 5 separate `if` blocks in `validate()`. +Think about how to represent each rule as **data** instead of **code**. +What if each rule were just a function in a list? diff --git a/password_validator/lib/password_validator.dart b/password_validator/lib/password_validator.dart new file mode 100644 index 0000000..67f1130 --- /dev/null +++ b/password_validator/lib/password_validator.dart @@ -0,0 +1,25 @@ +// TODO: Implement PasswordValidator using TDD. +// +// Rules (implement one at a time, test first!): +// 1. Must be at least 8 characters long +// 2. Must contain at least one uppercase letter +// 3. Must contain at least one digit +// 4. Must contain at least one special character (!@#$%^&*) +// 5. Must not contain spaces +// +// ValidationResult holds a pass/fail flag AND a list of error messages. +// Never return a boolean — the caller deserves to know *why* it failed. + +class ValidationResult { + final bool isValid; + final List errors; + + const ValidationResult({required this.isValid, required this.errors}); +} + +class PasswordValidator { + ValidationResult validate(String password) { + // TODO: implement + throw UnimplementedError(); + } +} diff --git a/password_validator/pubspec.yaml b/password_validator/pubspec.yaml new file mode 100644 index 0000000..4325b73 --- /dev/null +++ b/password_validator/pubspec.yaml @@ -0,0 +1,8 @@ +name: password_validator +description: TDD Workshop - Feature A + +environment: + sdk: ^3.0.0 + +dev_dependencies: + test: ^1.25.0 diff --git a/password_validator/test/password_validator_test.dart b/password_validator/test/password_validator_test.dart new file mode 100644 index 0000000..9895ab7 --- /dev/null +++ b/password_validator/test/password_validator_test.dart @@ -0,0 +1,157 @@ +import 'package:test/test.dart'; +import '../lib/password_validator.dart'; + +// ============================================================ +// PASSWORD VALIDATOR — Workshop Starter +// Follow Red → Green → Refactor strictly. +// Uncomment one test at a time. Make it pass. Then the next. +// ============================================================ + +void main() { + late PasswordValidator validator; + + setUp(() { + validator = PasswordValidator(); + }); + + // ────────────────────────────────────────────────────────── + // STEP 1 — Minimum length + // A password needs at least 8 characters. + // Start here. Everything else builds on a valid-length string. + // ────────────────────────────────────────────────────────── + group('Minimum length (8 characters)', () { + test('rejects a password shorter than 8 characters', () { + // TODO: uncomment when ready + // final result = validator.validate('Ab1!xyz'); + // expect(result.isValid, isFalse); + // expect(result.errors, contains('Must be at least 8 characters long')); + }); + + test('accepts a password that is exactly 8 characters', () { + // TODO: uncomment when ready + // final result = validator.validate('Ab1!xyzw'); + // expect(result.isValid, isTrue); // assuming other rules pass too + }); + + test('accepts a password longer than 8 characters', () { + // TODO: uncomment when ready + // final result = validator.validate('Ab1!xyzwqr'); + // expect(result.isValid, isTrue); + }); + }); + + // ────────────────────────────────────────────────────────── + // STEP 2 — Uppercase letter + // At least one A–Z character must be present. + // ────────────────────────────────────────────────────────── + group('Uppercase letter required', () { + test('rejects a password with no uppercase letters', () { + // TODO: uncomment when ready + // final result = validator.validate('ab1!xyzw'); + // expect(result.isValid, isFalse); + // expect(result.errors, contains('Must contain at least one uppercase letter')); + }); + + test('accepts a password with at least one uppercase letter', () { + // TODO: uncomment when ready + // final result = validator.validate('Ab1!xyzw'); + // expect(result.isValid, isTrue); + }); + }); + + // ────────────────────────────────────────────────────────── + // STEP 3 — Digit required + // At least one 0–9 character must be present. + // ────────────────────────────────────────────────────────── + group('Digit required', () { + test('rejects a password with no digits', () { + // TODO: uncomment when ready + // final result = validator.validate('Ab!!xyzw'); + // expect(result.isValid, isFalse); + // expect(result.errors, contains('Must contain at least one digit')); + }); + + test('accepts a password with at least one digit', () { + // TODO: uncomment when ready + // final result = validator.validate('Ab1!xyzw'); + // expect(result.isValid, isTrue); + }); + }); + + // ────────────────────────────────────────────────────────── + // STEP 4 — Special character required + // At least one of: ! @ # $ % ^ & * + // ────────────────────────────────────────────────────────── + group('Special character required', () { + test('rejects a password with no special characters', () { + // TODO: uncomment when ready + // final result = validator.validate('Ab1xxxyw'); + // expect(result.isValid, isFalse); + // expect(result.errors, contains('Must contain at least one special character (!@#\$%^&*)')); + }); + + test('accepts a password with at least one special character', () { + // TODO: uncomment when ready + // final result = validator.validate('Ab1!xyzw'); + // expect(result.isValid, isTrue); + }); + }); + + // ────────────────────────────────────────────────────────── + // STEP 5 — No spaces allowed + // ────────────────────────────────────────────────────────── + group('No spaces allowed', () { + test('rejects a password that contains a space', () { + // TODO: uncomment when ready + // final result = validator.validate('Ab1! xyz'); + // expect(result.isValid, isFalse); + // expect(result.errors, contains('Must not contain spaces')); + }); + + test('accepts a password with no spaces', () { + // TODO: uncomment when ready + // final result = validator.validate('Ab1!xyzw'); + // expect(result.isValid, isTrue); + }); + }); + + // ────────────────────────────────────────────────────────── + // STEP 6 — Multiple errors reported at once + // The validator collects ALL failures, not just the first. + // This is the refactor insight: rules should be independent. + // ────────────────────────────────────────────────────────── + group('Multiple errors reported together', () { + test('reports all failures for a completely invalid password', () { + // TODO: uncomment when ready + // final result = validator.validate('bad'); + // expect(result.isValid, isFalse); + // expect(result.errors.length, equals(4)); // length, uppercase, digit, special char + }); + + test('a fully valid password has no errors', () { + // TODO: uncomment when ready + // final result = validator.validate('MyP@ssw0rd'); + // expect(result.isValid, isTrue); + // expect(result.errors, isEmpty); + }); + }); + + // ────────────────────────────────────────────────────────── + // BONUS — Edge cases (if you finish early) + // ────────────────────────────────────────────────────────── + group('Edge cases', () { + test('empty string is invalid and reports the length error', () { + // TODO: uncomment when ready + // final result = validator.validate(''); + // expect(result.isValid, isFalse); + // expect(result.errors, contains('Must be at least 8 characters long')); + }); + + test('exactly the minimum: 8 chars, all rules satisfied', () { + // TODO: uncomment when ready + // final result = validator.validate('Aa1!aaaa'); + // expect(result.isValid, isTrue); + // expect(result.errors, isEmpty); + }); + }); +} diff --git a/shopping_cart/README.md b/shopping_cart/README.md new file mode 100644 index 0000000..8ca2b85 --- /dev/null +++ b/shopping_cart/README.md @@ -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!) + +
+Hint for Step 3 — Remove by name + +When you need to remove by name, a `List` forces you to search by name. +A `Map` makes lookup O(1) and removal trivial. +Let the test drive you toward the right data structure. +
+ +
+Hint for Step 5 — Value Object + +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. +
+ +
+Hint for the float precision bonus test + +Use `closeTo(expected, delta)` instead of `equals()` for floating point comparisons. +Example: `expect(result, closeTo(0.30, 0.001))` +
diff --git a/shopping_cart/lib/shopping_cart.dart b/shopping_cart/lib/shopping_cart.dart new file mode 100644 index 0000000..0e89a55 --- /dev/null +++ b/shopping_cart/lib/shopping_cart.dart @@ -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(); + } +} diff --git a/shopping_cart/pubspec.yaml b/shopping_cart/pubspec.yaml new file mode 100644 index 0000000..8c51a74 --- /dev/null +++ b/shopping_cart/pubspec.yaml @@ -0,0 +1,8 @@ +name: shopping_cart +description: TDD Workshop - Feature B + +environment: + sdk: ^3.0.0 + +dev_dependencies: + test: ^1.25.0 diff --git a/shopping_cart/test/shopping_cart_test.dart b/shopping_cart/test/shopping_cart_test.dart new file mode 100644 index 0000000..9530d01 --- /dev/null +++ b/shopping_cart/test/shopping_cart_test.dart @@ -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! + }); + }); +}