Initial commit: Complete TDD workshop materials
- 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
This commit is contained in:
commit
c3355063f2
26 changed files with 4725 additions and 0 deletions
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Dart build artifacts
|
||||||
|
.dart_tool/
|
||||||
|
.packages
|
||||||
|
pubspec.lock
|
||||||
|
build/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
161
FACILITATOR_GUIDE.md
Normal file
161
FACILITATOR_GUIDE.md
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
# Workshop Facilitator Guide — Quick Reference
|
||||||
|
|
||||||
|
**Duration:** 2 hours
|
||||||
|
**Format:** Live demo + hands-on practice with Password Validator or Shopping Cart katas
|
||||||
|
|
||||||
|
> **Note:** For the complete detailed plan with minute-by-minute timing, scripts, and checklists, see **WORKSHOP_PLAN.md**. This guide focuses on facilitation tips and kata-specific insights.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Essential Documents
|
||||||
|
|
||||||
|
Before the workshop, familiarize yourself with:
|
||||||
|
- **WORKSHOP_PLAN.md** — Complete schedule, scripts, and preparation checklist
|
||||||
|
- **fizzbuzz/DEMO_SCRIPT.md** — Step-by-step live demo walkthrough
|
||||||
|
- **TDD_REFERENCE_CARD.md** — Participant handout
|
||||||
|
- **SETUP_GUIDE.md** — For helping participants with technical issues
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Preparation Checklist
|
||||||
|
|
||||||
|
### 1 Week Before
|
||||||
|
- [ ] Send SETUP_GUIDE.md to participants
|
||||||
|
- [ ] Confirm everyone completes setup and verifies `dart test` works
|
||||||
|
- [ ] Practice FizzBuzz live demo using DEMO_SCRIPT.md
|
||||||
|
|
||||||
|
### Day Of
|
||||||
|
- [ ] Arrive 15 minutes early
|
||||||
|
- [ ] Test projector/screen sharing
|
||||||
|
- [ ] Open fizzbuzz kata with git log ready
|
||||||
|
- [ ] Have solution files ready (but don't share early!)
|
||||||
|
- [ ] Write schedule on whiteboard
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Updated Session Structure
|
||||||
|
|
||||||
|
| Time | Activity | See |
|
||||||
|
|------|----------|-----|
|
||||||
|
| 0:00-0:10 | Welcome & Setup Check | WORKSHOP_PLAN.md |
|
||||||
|
| 0:10-0:25 | **Live Demo: FizzBuzz TDD** | fizzbuzz/DEMO_SCRIPT.md |
|
||||||
|
| 0:25-0:30 | Exercise Introduction | Section below |
|
||||||
|
| 0:30-1:15 | Hands-on Practice (Part 1) | Circulate & nudge |
|
||||||
|
| 1:15-1:30 | Mid-Point Check-in | WORKSHOP_PLAN.md |
|
||||||
|
| 1:30-2:00 | Hands-on Practice (Part 2) | Circulate & nudge |
|
||||||
|
| 2:00-2:10 | Show & Tell | 2-3 volunteers |
|
||||||
|
| 2:10-2:20 | Retrospective | WORKSHOP_PLAN.md |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Live Demo: FizzBuzz (NEW SECTION)
|
||||||
|
|
||||||
|
**Purpose:** Show RED-GREEN-REFACTOR cycle in real-time
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
1. Open `fizzbuzz/` directory
|
||||||
|
2. Have `fizzbuzz/DEMO_SCRIPT.md` open on second screen
|
||||||
|
3. Terminal ready with `dart test`
|
||||||
|
4. Reset git to initial commit: `git reset --hard <first-commit-hash>`
|
||||||
|
|
||||||
|
### Key Points to Emphasize
|
||||||
|
- **Run tests frequently** (every 30 seconds)
|
||||||
|
- **Minimal implementations** (hardcode "1" for first test)
|
||||||
|
- **Test-driven design** (algorithm emerges from tests)
|
||||||
|
- **Order matters** (check 15 before 3 and 5)
|
||||||
|
|
||||||
|
### Timing
|
||||||
|
- 0-2 min: Introduce FizzBuzz rules
|
||||||
|
- 2-4 min: Cycle 1 (input 1 → "1")
|
||||||
|
- 4-6 min: Cycle 2 (input 2 → "2")
|
||||||
|
- 6-9 min: Cycle 3 (input 3 → "Fizz")
|
||||||
|
- 9-11 min: Cycle 4 (input 5 → "Buzz")
|
||||||
|
- 11-14 min: Cycle 5 (input 15 → "FizzBuzz")
|
||||||
|
- 14-15 min: Wrap up, show git log
|
||||||
|
|
||||||
|
**See fizzbuzz/DEMO_SCRIPT.md for complete talking points and what to code at each step.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What to watch for while circulating
|
||||||
|
|
||||||
|
**Good signs:**
|
||||||
|
- Running `dart test` after every small change
|
||||||
|
- Seeing RED before touching `lib/`
|
||||||
|
- Short, focused commits (or at least talking through each step)
|
||||||
|
|
||||||
|
**Gentle interventions:**
|
||||||
|
- *"Have you run the test yet? What did it say?"* — nudge anyone writing too much code before testing
|
||||||
|
- *"Can you make it pass with even less code?"* — push toward minimal Green
|
||||||
|
- *"Now that it's Green, what could you clean up?"* — prompt the Refactor step
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature A: Password Validator — facilitator notes
|
||||||
|
|
||||||
|
**Likely first implementation** (all rules as if-blocks):
|
||||||
|
```dart
|
||||||
|
ValidationResult validate(String password) {
|
||||||
|
final errors = <String>[];
|
||||||
|
if (password.length < 8) errors.add('Must be at least 8 characters long');
|
||||||
|
if (!password.contains(RegExp(r'[A-Z]'))) errors.add('Must contain at least one uppercase letter');
|
||||||
|
// ... etc
|
||||||
|
return ValidationResult(isValid: errors.isEmpty, errors: errors);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**The refactor insight to guide toward (Step 6):**
|
||||||
|
Each rule is just a function `String? Function(String)`. They can live in a list.
|
||||||
|
Adding a new rule = adding one function. No touching existing code.
|
||||||
|
This is the Open-Closed Principle — same insight as Gilded Rose's Strategy pattern.
|
||||||
|
|
||||||
|
**Talking point:** *"If you had 20 rules, how readable would 20 if-blocks be? What would you do?"*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature B: Shopping Cart — facilitator notes
|
||||||
|
|
||||||
|
**Likely first implementation** (List-based):
|
||||||
|
```dart
|
||||||
|
final List<CartItem> _items = [];
|
||||||
|
|
||||||
|
double subtotal() => _items.fold(0, (s, i) => s + i.price * i.quantity);
|
||||||
|
```
|
||||||
|
|
||||||
|
**The Map insight (Step 3 — remove by name):**
|
||||||
|
When participants try to implement `removeItem(name)` with a List, they'll write a `.removeWhere()`.
|
||||||
|
This works, but nudge them: *"What data structure makes 'find by name' really natural?"*
|
||||||
|
Let the remove-by-name test drive them toward `Map<String, CartItem>`.
|
||||||
|
|
||||||
|
**CartItem validation (Step 5):**
|
||||||
|
Many will put validation in `addItem()`. Ask: *"What if someone creates a CartItem and passes it somewhere else? Who validates it then?"*
|
||||||
|
This conversation leads naturally to Value Objects.
|
||||||
|
|
||||||
|
**Bonus design question:**
|
||||||
|
The duplicate name test has no right answer. Both "replace" and "accumulate" are valid.
|
||||||
|
The point is: TDD forces you to decide, and the test documents the decision permanently.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Closing discussion questions
|
||||||
|
|
||||||
|
1. *"What was the first test you wrote? Why that one?"*
|
||||||
|
2. *"Did your design change as you added more tests? How?"*
|
||||||
|
3. *"Did you ever feel tempted to skip the Red step and just write the code?"*
|
||||||
|
4. *"What would have been different if you'd designed the class first, then written tests?"*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common questions and answers
|
||||||
|
|
||||||
|
**"Can I write all the tests first, then implement?"**
|
||||||
|
Not quite — TDD is one test at a time. Write one, watch it fail, make it pass, then the next.
|
||||||
|
Writing all tests upfront is a different practice (it's fine, but it's not TDD).
|
||||||
|
|
||||||
|
**"What if I don't know what test to write next?"**
|
||||||
|
Ask: *"What's the simplest behavior this thing should have that it doesn't have yet?"*
|
||||||
|
Start with the empty/zero case. Then one item. Then two. Then edge cases.
|
||||||
|
|
||||||
|
**"Is this realistic? Do people actually do this for real code?"**
|
||||||
|
Yes — especially for domain logic (pricing, validation, calculations).
|
||||||
|
It's less common for framework glue code, and that's okay. Apply it where behavior matters most.
|
||||||
273
README.md
Normal file
273
README.md
Normal file
|
|
@ -0,0 +1,273 @@
|
||||||
|
# TDD Workshop
|
||||||
|
|
||||||
|
**A hands-on, 2-hour workshop for learning Test-Driven Development through deliberate practice.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workshop Overview
|
||||||
|
|
||||||
|
This workshop teaches the RED-GREEN-REFACTOR cycle through:
|
||||||
|
1. **Live demonstration** (FizzBuzz kata)
|
||||||
|
2. **Hands-on practice** (Password Validator or Shopping Cart)
|
||||||
|
3. **Group reflection** and retrospective
|
||||||
|
|
||||||
|
**Target audience:** Developers familiar with TDD concepts but lacking hands-on practice
|
||||||
|
**Duration:** 2 hours
|
||||||
|
**Language:** Dart
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Repository Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
tdd-workshop/
|
||||||
|
├── README.md (you are here) # Workshop overview
|
||||||
|
├── WORKSHOP_PLAN.md # Complete facilitator schedule & scripts
|
||||||
|
├── FACILITATOR_GUIDE.md # Quick reference for facilitators
|
||||||
|
├── TDD_REFERENCE_CARD.md # One-page handout for participants
|
||||||
|
├── SETUP_GUIDE.md # Installation & troubleshooting
|
||||||
|
│
|
||||||
|
├── fizzbuzz/ # Live demo kata
|
||||||
|
│ ├── README.md # Kata overview
|
||||||
|
│ ├── DEMO_SCRIPT.md # Step-by-step demo guide (15 min)
|
||||||
|
│ ├── lib/fizzbuzz.dart # Final implementation
|
||||||
|
│ └── test/fizzbuzz_test.dart # Progressive tests
|
||||||
|
│ └── .git/ # Git history shows TDD progression!
|
||||||
|
│
|
||||||
|
├── password_validator/ # Hands-on exercise (Option A)
|
||||||
|
│ ├── README.md # Exercise instructions
|
||||||
|
│ ├── DEMO_SCRIPT.md # Facilitator reference guide
|
||||||
|
│ ├── lib/
|
||||||
|
│ │ ├── password_validator.dart # Starter code (TODO)
|
||||||
|
│ │ └── password_validator_solution.dart # Reference solution
|
||||||
|
│ └── test/
|
||||||
|
│ └── password_validator_test.dart # Tests to uncomment step-by-step
|
||||||
|
│
|
||||||
|
└── shopping_cart/ # Hands-on exercise (Option B)
|
||||||
|
├── README.md # Exercise instructions
|
||||||
|
├── DEMO_SCRIPT.md # Facilitator reference guide
|
||||||
|
├── lib/
|
||||||
|
│ ├── shopping_cart.dart # Starter code (TODO)
|
||||||
|
│ └── shopping_cart_solution.dart # Reference solution
|
||||||
|
└── test/
|
||||||
|
└── shopping_cart_test.dart # Tests to uncomment step-by-step
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## For Facilitators
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
1. **Read the plan:**
|
||||||
|
- Start with `WORKSHOP_PLAN.md` for complete details
|
||||||
|
- Review `FACILITATOR_GUIDE.md` for quick reference
|
||||||
|
- Study `fizzbuzz/DEMO_SCRIPT.md` for the live demo
|
||||||
|
- Review kata demo scripts:
|
||||||
|
- `password_validator/DEMO_SCRIPT.md` (reference for helping stuck participants)
|
||||||
|
- `shopping_cart/DEMO_SCRIPT.md` (reference for helping stuck participants)
|
||||||
|
|
||||||
|
2. **Practice the demo:**
|
||||||
|
```bash
|
||||||
|
cd fizzbuzz
|
||||||
|
git log --oneline # See the RED-GREEN-REFACTOR commits
|
||||||
|
```
|
||||||
|
Each commit shows one step of the TDD cycle.
|
||||||
|
|
||||||
|
3. **Prepare materials:**
|
||||||
|
- See "Pre-Workshop Preparation" in `WORKSHOP_PLAN.md`
|
||||||
|
- Share `SETUP_GUIDE.md` with participants 1 week before
|
||||||
|
- Have `TDD_REFERENCE_CARD.md` ready to share
|
||||||
|
|
||||||
|
4. **Day of workshop:**
|
||||||
|
- Arrive 15 minutes early
|
||||||
|
- Test projector/screen sharing
|
||||||
|
- Write schedule on whiteboard
|
||||||
|
- Have solution files ready (don't share early!)
|
||||||
|
|
||||||
|
### Key Documents
|
||||||
|
|
||||||
|
| Document | Purpose |
|
||||||
|
|----------|---------|
|
||||||
|
| **WORKSHOP_PLAN.md** | Complete schedule, scripts, checklists |
|
||||||
|
| **FACILITATOR_GUIDE.md** | Quick reference, kata insights |
|
||||||
|
| **fizzbuzz/DEMO_SCRIPT.md** | Minute-by-minute live demo walkthrough |
|
||||||
|
| **password_validator/DEMO_SCRIPT.md** | Reference guide for helping stuck participants |
|
||||||
|
| **shopping_cart/DEMO_SCRIPT.md** | Reference guide for helping stuck participants |
|
||||||
|
| **TDD_REFERENCE_CARD.md** | Participant handout (print or share digitally) |
|
||||||
|
| **SETUP_GUIDE.md** | For participants before workshop |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## For Participants
|
||||||
|
|
||||||
|
### Before the Workshop
|
||||||
|
|
||||||
|
1. **Complete setup:** Follow `SETUP_GUIDE.md` to install Dart and verify tests run
|
||||||
|
2. **Verify installation:**
|
||||||
|
```bash
|
||||||
|
cd password_validator
|
||||||
|
dart pub get
|
||||||
|
dart test # Should see "All tests skipped" or passing
|
||||||
|
```
|
||||||
|
3. **Bookmark reference:** Have `TDD_REFERENCE_CARD.md` ready during the workshop
|
||||||
|
|
||||||
|
### During the Workshop
|
||||||
|
|
||||||
|
1. **Watch the live demo** — See RED-GREEN-REFACTOR in action (FizzBuzz)
|
||||||
|
2. **Choose a kata:**
|
||||||
|
- **Password Validator:** Rules-based validation, interesting refactoring
|
||||||
|
- **Shopping Cart:** Stateful domain object, data structure decisions
|
||||||
|
3. **Follow the workflow:**
|
||||||
|
- Uncomment one test at a time
|
||||||
|
- Make it RED (watch it fail)
|
||||||
|
- Make it GREEN (minimal code to pass)
|
||||||
|
- Refactor (clean up duplication)
|
||||||
|
- Repeat
|
||||||
|
|
||||||
|
### Kata Instructions
|
||||||
|
|
||||||
|
#### Password Validator
|
||||||
|
- Open `password_validator/README.md` for instructions
|
||||||
|
- Implement validation rules one at a time
|
||||||
|
- Step 6 is the refactoring challenge (rules as data)
|
||||||
|
|
||||||
|
#### Shopping Cart
|
||||||
|
- Open `shopping_cart/README.md` for instructions
|
||||||
|
- Build a shopping cart with add, remove, total, discount
|
||||||
|
- Discover when to use Map vs List (Step 3)
|
||||||
|
- Learn Value Object pattern (Step 5)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What You'll Learn
|
||||||
|
|
||||||
|
### The TDD Rhythm
|
||||||
|
- **RED:** Write a failing test (proves it can fail)
|
||||||
|
- **GREEN:** Write minimal code to pass (make it work)
|
||||||
|
- **REFACTOR:** Clean up code (make it right)
|
||||||
|
- **REPEAT:** Next test
|
||||||
|
|
||||||
|
### Design Insights
|
||||||
|
|
||||||
|
Through hands-on practice, you'll discover:
|
||||||
|
|
||||||
|
**Password Validator:**
|
||||||
|
- How duplication in tests reveals duplication in code
|
||||||
|
- Rules as data (Open-Closed Principle)
|
||||||
|
- Refactoring with tests as a safety net
|
||||||
|
|
||||||
|
**Shopping Cart:**
|
||||||
|
- How tests drive data structure choices (List → Map)
|
||||||
|
- Value Objects for validation
|
||||||
|
- Stateful domain modeling
|
||||||
|
|
||||||
|
**General:**
|
||||||
|
- Tests as living documentation
|
||||||
|
- Design emerges from simple tests
|
||||||
|
- Small steps lead to robust solutions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## After the Workshop
|
||||||
|
|
||||||
|
### Continue Practicing
|
||||||
|
|
||||||
|
Explore the **tdd-katas** repository for more practice:
|
||||||
|
https://github.com/dhemasnurjaya/tdd-katas
|
||||||
|
|
||||||
|
It contains 5 complete katas with git commit history showing every RED-GREEN-REFACTOR step:
|
||||||
|
1. **Roman Numerals** — Table-driven algorithms
|
||||||
|
2. **Bowling Game** — State management and look-ahead logic
|
||||||
|
3. **Gilded Rose** — Refactoring legacy code safely
|
||||||
|
4. **String Calculator** — Bug hunting with TDD
|
||||||
|
5. **Mars Rover** — Command pattern and value objects
|
||||||
|
|
||||||
|
### Resources
|
||||||
|
|
||||||
|
**Reference materials:**
|
||||||
|
- `TDD_REFERENCE_CARD.md` — Quick reference for TDD practice
|
||||||
|
|
||||||
|
**Books:**
|
||||||
|
- *Test-Driven Development* by Kent Beck
|
||||||
|
- *Clean Code* by Robert C. Martin
|
||||||
|
- *Growing Object-Oriented Software, Guided by Tests* by Freeman & Pryce
|
||||||
|
|
||||||
|
**Online practice:**
|
||||||
|
- Kata-Log: https://kata-log.rocks
|
||||||
|
- Exercism: https://exercism.org
|
||||||
|
- Coding Dojo: https://codingdojo.org
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 (Demo)
|
||||||
|
```bash
|
||||||
|
cd fizzbuzz
|
||||||
|
dart pub get
|
||||||
|
dart test
|
||||||
|
|
||||||
|
# See TDD progression through git history
|
||||||
|
git log --oneline
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
See `SETUP_GUIDE.md` for detailed troubleshooting of common issues:
|
||||||
|
- Dart installation problems
|
||||||
|
- `dart pub get` failures
|
||||||
|
- Test execution errors
|
||||||
|
- IDE setup
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Questions?
|
||||||
|
|
||||||
|
**For facilitators:**
|
||||||
|
- Review `WORKSHOP_PLAN.md` for comprehensive guidance
|
||||||
|
- Check `FACILITATOR_GUIDE.md` for kata-specific insights
|
||||||
|
|
||||||
|
**For participants:**
|
||||||
|
- During workshop: Raise your hand or ask facilitator
|
||||||
|
- Before workshop: Contact your facilitator with setup questions
|
||||||
|
- After workshop: Continue practicing with tdd-katas repository
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Philosophy
|
||||||
|
|
||||||
|
> "Clean code that works." — Ron Jeffries
|
||||||
|
|
||||||
|
TDD is a **discipline**, not a burden. The rhythm becomes automatic with practice:
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This workshop material is provided for educational purposes. Feel free to adapt and use for your own workshops.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Ready to start? Facilitators: See `WORKSHOP_PLAN.md`. Participants: See `SETUP_GUIDE.md`.**
|
||||||
434
SETUP_GUIDE.md
Normal file
434
SETUP_GUIDE.md
Normal file
|
|
@ -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 <repository-url>
|
||||||
|
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. 🚀
|
||||||
276
TDD_REFERENCE_CARD.md
Normal file
276
TDD_REFERENCE_CARD.md
Normal file
|
|
@ -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<String>());
|
||||||
|
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<DivisionByZeroException>()));
|
||||||
|
|
||||||
|
// 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.**
|
||||||
520
WORKSHOP_PLAN.md
Normal file
520
WORKSHOP_PLAN.md
Normal file
|
|
@ -0,0 +1,520 @@
|
||||||
|
# TDD Workshop - Complete Plan
|
||||||
|
|
||||||
|
**Title:** Test-Driven Development Workshop: From Theory to Practice
|
||||||
|
**Duration:** 2 hours (120 minutes)
|
||||||
|
**Format:** In-person, hands-on
|
||||||
|
**Target Audience:** Developers familiar with TDD concepts but lacking hands-on practice
|
||||||
|
**Language:** Dart
|
||||||
|
**Primary Goal:** Build muscle memory for the RED-GREEN-REFACTOR cycle through deliberate practice
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pre-Workshop Preparation
|
||||||
|
|
||||||
|
### 1 Week Before
|
||||||
|
- [ ] Confirm participant count and send calendar invite
|
||||||
|
- [ ] Share repository link with setup instructions (SETUP_GUIDE.md)
|
||||||
|
- [ ] Ask participants to complete setup and verify `dart pub get` + `dart test` work
|
||||||
|
- [ ] Prepare a backup laptop with environment pre-configured
|
||||||
|
- [ ] Review this plan and the FACILITATOR_GUIDE.md
|
||||||
|
|
||||||
|
### 1 Day Before
|
||||||
|
- [ ] Practice FizzBuzz live demo (use DEMO_SCRIPT.md)
|
||||||
|
- [ ] Review password_validator and shopping_cart solution files
|
||||||
|
- [ ] Test all katas on your machine (`dart test` in each directory)
|
||||||
|
- [ ] Print this workshop plan as backup reference
|
||||||
|
- [ ] Prepare any slides (optional - just 3-4 slides max)
|
||||||
|
|
||||||
|
### Morning of Workshop
|
||||||
|
- [ ] Arrive 15 minutes early to set up room
|
||||||
|
- [ ] Test projector / screen sharing
|
||||||
|
- [ ] Open all relevant files in your editor:
|
||||||
|
- fizzbuzz/lib/fizzbuzz.dart
|
||||||
|
- fizzbuzz/test/fizzbuzz_test.dart
|
||||||
|
- fizzbuzz/DEMO_SCRIPT.md
|
||||||
|
- [ ] Have terminal ready with `dart test` command
|
||||||
|
- [ ] Write schedule on whiteboard for participant reference
|
||||||
|
- [ ] Open git log for FizzBuzz: `git log --oneline`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Materials Checklist
|
||||||
|
|
||||||
|
### Digital Materials
|
||||||
|
- [ ] Workshop repository cloned and working
|
||||||
|
- [ ] FizzBuzz kata prepared for live demo
|
||||||
|
- [ ] TDD_REFERENCE_CARD.md ready to share
|
||||||
|
- [ ] Link to tdd-katas repo bookmarked (for post-workshop)
|
||||||
|
- [ ] Backup: Repository on USB drive (if network fails)
|
||||||
|
|
||||||
|
### Physical Materials
|
||||||
|
- [ ] Whiteboard and markers (for schedule and collecting insights)
|
||||||
|
- [ ] Sticky notes (for participants to write questions)
|
||||||
|
- [ ] This printed workshop plan
|
||||||
|
- [ ] FACILITATOR_GUIDE.md printed as backup
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Detailed Schedule
|
||||||
|
|
||||||
|
| Start | Duration | Segment | Format |
|
||||||
|
|-------|----------|---------|--------|
|
||||||
|
| 0:00 | 10 min | Welcome & Setup Check | Facilitator-led |
|
||||||
|
| 0:10 | 15 min | Live Demo: FizzBuzz TDD | Live-coding |
|
||||||
|
| 0:25 | 5 min | Exercise Introduction | Facilitator-led |
|
||||||
|
| 0:30 | 45 min | Hands-on Practice (Part 1) | Solo/pairs coding |
|
||||||
|
| 1:15 | 15 min | Mid-Point Check-in | Group discussion |
|
||||||
|
| 1:30 | 30 min | Hands-on Practice (Part 2) | Solo/pairs coding |
|
||||||
|
| 2:00 | 10 min | Show & Tell | 2-3 volunteers |
|
||||||
|
| 2:10 | 10 min | Retrospective Discussion | Facilitated |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Segment-by-Segment Guide
|
||||||
|
|
||||||
|
### Segment 1: Welcome & Setup (0:00-0:10) — 10 minutes
|
||||||
|
|
||||||
|
**Objectives:**
|
||||||
|
- Ensure everyone can run tests
|
||||||
|
- Set expectations
|
||||||
|
- Create psychological safety
|
||||||
|
|
||||||
|
**Script:**
|
||||||
|
```
|
||||||
|
Welcome! Today we're building TDD muscle memory through practice.
|
||||||
|
|
||||||
|
Quick poll:
|
||||||
|
- Written unit tests before? [Most hands should go up]
|
||||||
|
- Tried writing tests BEFORE code? [Some hands]
|
||||||
|
- Do it regularly, every day? [Few/no hands]
|
||||||
|
|
||||||
|
That's totally normal. Today is about practice, not theory.
|
||||||
|
|
||||||
|
Setup check - everyone run:
|
||||||
|
cd tdd-workshop/password_validator
|
||||||
|
dart pub get
|
||||||
|
dart test
|
||||||
|
|
||||||
|
You should see "All tests skipped" or passing tests. Raise your hand if you see errors.
|
||||||
|
|
||||||
|
The rhythm we'll practice:
|
||||||
|
1. RED - Write a test, watch it fail
|
||||||
|
2. GREEN - Write minimal code to pass
|
||||||
|
3. REFACTOR - Clean up
|
||||||
|
4. REPEAT
|
||||||
|
|
||||||
|
Let's see this in action.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Actions:**
|
||||||
|
- [ ] Display repo URL on screen
|
||||||
|
- [ ] Walk around to help with setup issues
|
||||||
|
- [ ] Ensure everyone has green/skipped tests
|
||||||
|
|
||||||
|
**What Success Looks Like:**
|
||||||
|
- Everyone has tests running
|
||||||
|
- No one is stuck on setup
|
||||||
|
- Energy is positive and relaxed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Segment 2: Live Demo - FizzBuzz (0:10-0:25) — 15 minutes
|
||||||
|
|
||||||
|
**Objectives:**
|
||||||
|
- Demonstrate RED-GREEN-REFACTOR in real-time
|
||||||
|
- Show what "minimal implementation" means
|
||||||
|
- Model thinking process out loud
|
||||||
|
|
||||||
|
**Preparation:**
|
||||||
|
- Open fizzbuzz directory
|
||||||
|
- Have DEMO_SCRIPT.md open on second screen
|
||||||
|
- Terminal ready to run `dart test`
|
||||||
|
|
||||||
|
**Follow the detailed script in `fizzbuzz/DEMO_SCRIPT.md`**
|
||||||
|
|
||||||
|
**Key Talking Points:**
|
||||||
|
- "Notice I run tests every 30 seconds"
|
||||||
|
- "This looks silly but it passes"
|
||||||
|
- "The tests are documenting the rules"
|
||||||
|
- "I didn't design this upfront - it emerged"
|
||||||
|
- "Order matters! (15 before 3 and 5)"
|
||||||
|
|
||||||
|
**What Success Looks Like:**
|
||||||
|
- Completed 5 RED-GREEN cycles in 15 minutes
|
||||||
|
- Participants see the rhythm clearly
|
||||||
|
- At least one "aha!" moment visible (nodding, note-taking)
|
||||||
|
|
||||||
|
**Time Check:** Should finish by 0:25
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Segment 3: Exercise Introduction (0:25-0:30) — 5 minutes
|
||||||
|
|
||||||
|
**Objectives:**
|
||||||
|
- Explain the two kata choices
|
||||||
|
- Clarify workflow
|
||||||
|
- Get people started
|
||||||
|
|
||||||
|
**Script:**
|
||||||
|
```
|
||||||
|
Now it's your turn. Two katas to choose from:
|
||||||
|
|
||||||
|
PASSWORD VALIDATOR:
|
||||||
|
- Rules-based validation (length, uppercase, digits...)
|
||||||
|
- Step 6 has the interesting refactoring challenge
|
||||||
|
- Good for practicing: extracting patterns, Open-Closed Principle
|
||||||
|
|
||||||
|
SHOPPING CART:
|
||||||
|
- Stateful domain object (add, remove, calculate)
|
||||||
|
- You'll discover when to use Map vs List
|
||||||
|
- Good for practicing: Value Objects, data structure decisions
|
||||||
|
|
||||||
|
Both are good. Pick what sounds interesting.
|
||||||
|
|
||||||
|
Workflow:
|
||||||
|
1. Open the test file
|
||||||
|
2. Uncomment ONE test group
|
||||||
|
3. Run dart test - see it RED
|
||||||
|
4. Write code to make it GREEN
|
||||||
|
5. Refactor if needed
|
||||||
|
6. Next test
|
||||||
|
|
||||||
|
Pairing optional - if you pair, try "ping-pong":
|
||||||
|
- Person A writes test
|
||||||
|
- Person B makes it pass
|
||||||
|
- Switch roles
|
||||||
|
|
||||||
|
Questions? Good. Choose your kata and start with Step 1!
|
||||||
|
```
|
||||||
|
|
||||||
|
**Actions:**
|
||||||
|
- [ ] Show README files on screen briefly
|
||||||
|
- [ ] Start 45-minute timer
|
||||||
|
- [ ] Let people move to pair up if desired
|
||||||
|
|
||||||
|
**What Success Looks Like:**
|
||||||
|
- Everyone has chosen a kata
|
||||||
|
- Files are open
|
||||||
|
- First test is being uncommented
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Segment 4: Hands-on Practice Part 1 (0:30-1:15) — 45 minutes
|
||||||
|
|
||||||
|
**Objectives:**
|
||||||
|
- Build RED-GREEN-REFACTOR muscle memory
|
||||||
|
- Experience how tests drive design
|
||||||
|
- Encounter and resolve "stuck" moments
|
||||||
|
|
||||||
|
**Your Role:** Circulate, observe, nudge with questions (don't give solutions)
|
||||||
|
|
||||||
|
**Good Signs:**
|
||||||
|
- Tests running frequently
|
||||||
|
- Seeing RED before touching lib/
|
||||||
|
- Small commits or at least incremental changes
|
||||||
|
- Discussing design (if pairing)
|
||||||
|
|
||||||
|
**Interventions (Use these phrases):**
|
||||||
|
|
||||||
|
| Situation | What to Say |
|
||||||
|
|-----------|-------------|
|
||||||
|
| Writing code without test | "Have you run the test yet? What did it say?" |
|
||||||
|
| Multiple tests at once | "Let's focus on making just ONE test pass first" |
|
||||||
|
| Overthinking | "What's the simplest thing that could work?" |
|
||||||
|
| Not refactoring | "See any duplication? What could you extract?" |
|
||||||
|
| Stuck on data structure | "What makes 'find by name' really easy?" (Shopping Cart) |
|
||||||
|
| Hardcoding too long | "Try another test with different input. What happens?" |
|
||||||
|
|
||||||
|
**Guiding Questions:**
|
||||||
|
- "What's the next simplest test?"
|
||||||
|
- "Can you pass with even less code?"
|
||||||
|
- "Now that it's green, what needs cleaning up?"
|
||||||
|
- "How would this scale to 20 rules?" (Password)
|
||||||
|
- "Why List vs Map here?" (Shopping Cart)
|
||||||
|
|
||||||
|
**Time Checks:**
|
||||||
|
- **At 15 min (0:45):** Most should be on Step 2-3
|
||||||
|
- **At 30 min (1:00):** Most should be on Step 3-4
|
||||||
|
- **At 40 min (1:10):** Some on Step 5-6, some still on 3-4 (both fine)
|
||||||
|
|
||||||
|
**What Success Looks Like:**
|
||||||
|
- Steady typing and test running
|
||||||
|
- Some "aha!" moments (facial expressions, discussions)
|
||||||
|
- A few stuck moments that resolve with nudging
|
||||||
|
- Energy remains positive
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Segment 5: Mid-Point Check-in (1:15-1:30) — 15 minutes
|
||||||
|
|
||||||
|
**Objectives:**
|
||||||
|
- Surface common struggles
|
||||||
|
- Share discoveries
|
||||||
|
- Re-energize
|
||||||
|
|
||||||
|
**Script:**
|
||||||
|
```
|
||||||
|
Alright, pause for a moment. Stretch if you need to.
|
||||||
|
|
||||||
|
Quick progress check:
|
||||||
|
- Who's completed Steps 1-2? [Show of hands]
|
||||||
|
- Step 3-4? [Hands]
|
||||||
|
- Step 5 or beyond? [Hands]
|
||||||
|
|
||||||
|
Good mix!
|
||||||
|
|
||||||
|
What surprised you so far?
|
||||||
|
[Let 2-3 people share - listen for:]
|
||||||
|
- How often tests run
|
||||||
|
- Refactor step insights
|
||||||
|
- Data structure discoveries (Map vs List)
|
||||||
|
|
||||||
|
Anyone get stuck? What helped you get unstuck?
|
||||||
|
[Usually: reading test carefully, trying minimal solution]
|
||||||
|
|
||||||
|
Any design insights?
|
||||||
|
[Password folks might mention: rules as functions
|
||||||
|
Shopping Cart folks might mention: Value Objects]
|
||||||
|
|
||||||
|
Okay, 30 more minutes. Options:
|
||||||
|
- Continue your current kata
|
||||||
|
- If finished, try the other one
|
||||||
|
- If deep in refactoring, keep going
|
||||||
|
|
||||||
|
The TDD_REFERENCE_CARD.md is available if you need a reminder on anything.
|
||||||
|
|
||||||
|
Let's go!
|
||||||
|
```
|
||||||
|
|
||||||
|
**Actions:**
|
||||||
|
- [ ] Capture themes on whiteboard
|
||||||
|
- [ ] Validate their experiences ("Yes! That's exactly what TDD teaches")
|
||||||
|
- [ ] Note time remaining
|
||||||
|
|
||||||
|
**What Success Looks Like:**
|
||||||
|
- Participants feel heard and validated
|
||||||
|
- Common struggles are normalized
|
||||||
|
- Energy is refreshed for part 2
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Segment 6: Hands-on Practice Part 2 (1:30-2:00) — 30 minutes
|
||||||
|
|
||||||
|
**Objectives:**
|
||||||
|
- Complete current kata OR start second one
|
||||||
|
- Experience full cycle including refactoring
|
||||||
|
|
||||||
|
**Your Role:**
|
||||||
|
- Continue circulating
|
||||||
|
- Focus attention on people doing Step 6 (Password) or Value Objects (Cart)
|
||||||
|
- Encourage those who finished to try the other kata
|
||||||
|
|
||||||
|
**Deep Dive Conversations:**
|
||||||
|
|
||||||
|
**Password Validator (Step 6):**
|
||||||
|
```
|
||||||
|
Them: "I have 5 if-blocks. Works but feels repetitive."
|
||||||
|
You: "What if each rule was a function? What would that look like?"
|
||||||
|
[Pause - let them think]
|
||||||
|
You: "Each rule is: String → Optional Error, right? Could you make a list?"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Shopping Cart (Value Object):**
|
||||||
|
```
|
||||||
|
Them: "Should I validate price in ShoppingCart.addItem()?"
|
||||||
|
You: "What if someone creates CartItem outside the cart? Who validates?"
|
||||||
|
[Pause - let them think]
|
||||||
|
You: "What if CartItem couldn't be created invalid? Where would validation go?"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Time Checks:**
|
||||||
|
- **At 10 min (1:40):** Give heads up: "20 minutes left"
|
||||||
|
- **At 20 min (1:50):** Give heads up: "10 minutes - finish your current test"
|
||||||
|
- **At 28 min (1:58):** "2 minutes - get to a stopping point"
|
||||||
|
|
||||||
|
**What Success Looks Like:**
|
||||||
|
- Most finish Step 4-5 or beyond
|
||||||
|
- Some discover refactoring patterns
|
||||||
|
- Code is messy but working (that's fine!)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Segment 7: Show & Tell (2:00-2:10) — 10 minutes
|
||||||
|
|
||||||
|
**Objectives:**
|
||||||
|
- See different approaches
|
||||||
|
- Celebrate progress
|
||||||
|
- Learn from peers
|
||||||
|
|
||||||
|
**Script:**
|
||||||
|
```
|
||||||
|
Time's up! Let's see what you built.
|
||||||
|
|
||||||
|
I'd like 2-3 volunteers to share briefly:
|
||||||
|
1. Which tests are passing?
|
||||||
|
2. Show your implementation
|
||||||
|
3. ONE interesting decision you made
|
||||||
|
|
||||||
|
Who wants to go first?
|
||||||
|
```
|
||||||
|
|
||||||
|
**What to Highlight:**
|
||||||
|
- Test organization and naming
|
||||||
|
- Simple → complex progression
|
||||||
|
- Refactorings (if they reached Step 6)
|
||||||
|
- Different valid approaches (e.g., replace vs accumulate in cart)
|
||||||
|
|
||||||
|
**Feedback Template:**
|
||||||
|
```
|
||||||
|
"I love that you [specific good practice].
|
||||||
|
Look at how tests tell a story: [point to names].
|
||||||
|
This design [refactoring] is the [pattern name] pattern."
|
||||||
|
```
|
||||||
|
|
||||||
|
**What Success Looks Like:**
|
||||||
|
- 2-3 people share
|
||||||
|
- Variety in progress levels shown
|
||||||
|
- Participants see multiple valid approaches
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Segment 8: Retrospective (2:10-2:20) — 10 minutes
|
||||||
|
|
||||||
|
**Objectives:**
|
||||||
|
- Solidify learning
|
||||||
|
- Connect to daily work
|
||||||
|
- Inspire continued practice
|
||||||
|
|
||||||
|
**Discussion Questions:**
|
||||||
|
|
||||||
|
**1. "What was different about writing tests FIRST vs AFTER?"**
|
||||||
|
|
||||||
|
Expected answers:
|
||||||
|
- Tests were easier to write
|
||||||
|
- Design emerged naturally
|
||||||
|
- Caught edge cases earlier
|
||||||
|
- Felt slower at first, faster later
|
||||||
|
|
||||||
|
**2. "Did you feel tempted to skip the RED step?"**
|
||||||
|
|
||||||
|
Expected: "Yes!"
|
||||||
|
|
||||||
|
Your response: "That's the discipline. RED proves the test can fail."
|
||||||
|
|
||||||
|
**3. "What design insight surprised you?"**
|
||||||
|
|
||||||
|
Reinforce:
|
||||||
|
- Password: Rules as data (Open-Closed Principle)
|
||||||
|
- Shopping Cart: Map for lookup, Value Objects for validation
|
||||||
|
- General: Small steps → emergent design
|
||||||
|
|
||||||
|
**4. "How would you apply this at work?"**
|
||||||
|
|
||||||
|
Let them think out loud:
|
||||||
|
- "That pricing calculation module..."
|
||||||
|
- "Our validation logic could be structured like this..."
|
||||||
|
- "Next util function, test first"
|
||||||
|
|
||||||
|
**Closing Message:**
|
||||||
|
```
|
||||||
|
TDD is a skill. Like any skill, it feels awkward at first.
|
||||||
|
|
||||||
|
The rhythm - RED, GREEN, REFACTOR - becomes automatic with practice.
|
||||||
|
|
||||||
|
Three takeaways:
|
||||||
|
1. Tests first - even for "obvious" code
|
||||||
|
2. Small steps - one test at a time
|
||||||
|
3. Refactor - don't skip this
|
||||||
|
|
||||||
|
Want more practice? The tdd-katas repo has 5 complete examples with commit history showing every step:
|
||||||
|
[Show link on screen]
|
||||||
|
|
||||||
|
Thanks for participating! Questions?
|
||||||
|
```
|
||||||
|
|
||||||
|
**Actions:**
|
||||||
|
- [ ] Share link to tdd-katas repo
|
||||||
|
- [ ] Share TDD_REFERENCE_CARD.md
|
||||||
|
- [ ] Mention optional office hours if offering them
|
||||||
|
|
||||||
|
**What Success Looks Like:**
|
||||||
|
- Clear takeaways articulated
|
||||||
|
- Participants know where to practice next
|
||||||
|
- Positive energy to continue learning
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Facilitation Tips
|
||||||
|
|
||||||
|
### Energy Management
|
||||||
|
|
||||||
|
| Time | Energy Level | Your Role |
|
||||||
|
|------|-------------|-----------|
|
||||||
|
| 0-10 min | High | Welcoming, enthusiastic |
|
||||||
|
| 10-25 min | Focused | Demonstrating, teaching |
|
||||||
|
| 25-30 min | Enthusiastic | Clarifying, motivating |
|
||||||
|
| 30-1:15 | Calm | Supportive, circulating |
|
||||||
|
| 1:15-1:30 | Re-energize | Celebrating, validating |
|
||||||
|
| 1:30-2:00 | Encouraging | Nudging toward completion |
|
||||||
|
| 2:00-2:20 | Reflective | Connecting dots, inspiring |
|
||||||
|
|
||||||
|
### Common Questions and Answers
|
||||||
|
|
||||||
|
| Question | Answer |
|
||||||
|
|----------|--------|
|
||||||
|
| "Should I write all tests first?" | "No - one at a time. Write one, pass it, then next." |
|
||||||
|
| "Can I look at the solution?" | "Try 5 more minutes first. What's the smallest step?" |
|
||||||
|
| "This feels slow. Do people really do this?" | "Feels slow when learning. With practice, faster than debugging." |
|
||||||
|
| "What test should I write next?" | "Simplest behavior it doesn't handle. Start empty, then one, then two." |
|
||||||
|
| "Is TDD for everything?" | "It shines for domain logic. Less for UI layouts or framework glue." |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Success Metrics
|
||||||
|
|
||||||
|
At workshop end, participants should:
|
||||||
|
- [ ] Explain RED-GREEN-REFACTOR cycle
|
||||||
|
- [ ] Demonstrate writing failing test before code
|
||||||
|
- [ ] Recognize when to refactor
|
||||||
|
- [ ] Have experienced at least one design insight
|
||||||
|
- [ ] Be able to apply TDD to simple problems independently
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Post-Workshop Actions
|
||||||
|
|
||||||
|
**Immediately After:**
|
||||||
|
- [ ] Share tdd-katas repo link
|
||||||
|
- [ ] Send feedback form (3-5 questions)
|
||||||
|
- [ ] Note confusing points to improve next time
|
||||||
|
|
||||||
|
**Optional Follow-up:**
|
||||||
|
- [ ] 1 week later: Email with additional resources
|
||||||
|
- [ ] Offer "office hours" for questions
|
||||||
|
- [ ] Create Slack channel for ongoing TDD discussion
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Emergency Adjustments
|
||||||
|
|
||||||
|
### Running Behind Schedule
|
||||||
|
|
||||||
|
- **Skip:** Detailed mid-point discussion (keep it to 5 min)
|
||||||
|
- **Skip:** Show & tell (or reduce to 1 person, 3 min)
|
||||||
|
- **Keep:** Live demo and hands-on time
|
||||||
|
|
||||||
|
### Running Ahead of Schedule
|
||||||
|
|
||||||
|
- **Add:** 6th test to FizzBuzz demo (input 6 → "Fizz")
|
||||||
|
- **Add:** Bonus questions in retrospective
|
||||||
|
- **Add:** Quick discussion of when NOT to use TDD
|
||||||
|
|
||||||
|
### Technical Difficulties
|
||||||
|
|
||||||
|
- **Network down:** Use USB backup of repo
|
||||||
|
- **Projector fails:** Do demo at participant's screen, others gather
|
||||||
|
- **Participant computer issues:** Pair them with someone or use backup laptop
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**You're ready to facilitate! Follow this plan, stay flexible, and remember: their success is your success.** 🚀
|
||||||
733
docs/fizzbuzz-git-history.patch
Normal file
733
docs/fizzbuzz-git-history.patch
Normal file
|
|
@ -0,0 +1,733 @@
|
||||||
|
From 71fbd5a6f30e2c7f6e5e49807cae18274bc44506 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Dhemas Nurjaya <dhemasnurjaya@outlook.com>
|
||||||
|
Date: Tue, 10 Mar 2026 11:44:55 +0700
|
||||||
|
Subject: [PATCH 01/12] Initial setup: Create FizzBuzz kata structure
|
||||||
|
|
||||||
|
---
|
||||||
|
lib/fizzbuzz.dart | 1 +
|
||||||
|
pubspec.yaml | 9 +++++++++
|
||||||
|
test/fizzbuzz_test.dart | 6 ++++++
|
||||||
|
3 files changed, 16 insertions(+)
|
||||||
|
create mode 100644 lib/fizzbuzz.dart
|
||||||
|
create mode 100644 pubspec.yaml
|
||||||
|
create mode 100644 test/fizzbuzz_test.dart
|
||||||
|
|
||||||
|
diff --git a/lib/fizzbuzz.dart b/lib/fizzbuzz.dart
|
||||||
|
new file mode 100644
|
||||||
|
index 0000000..8c2215b
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/lib/fizzbuzz.dart
|
||||||
|
@@ -0,0 +1 @@
|
||||||
|
+// FizzBuzz kata - empty implementation file
|
||||||
|
diff --git a/pubspec.yaml b/pubspec.yaml
|
||||||
|
new file mode 100644
|
||||||
|
index 0000000..5e6dee3
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/pubspec.yaml
|
||||||
|
@@ -0,0 +1,9 @@
|
||||||
|
+name: fizzbuzz
|
||||||
|
+description: FizzBuzz kata for TDD workshop live demonstration
|
||||||
|
+version: 1.0.0
|
||||||
|
+
|
||||||
|
+environment:
|
||||||
|
+ sdk: ^3.0.0
|
||||||
|
+
|
||||||
|
+dev_dependencies:
|
||||||
|
+ test: ^1.24.0
|
||||||
|
diff --git a/test/fizzbuzz_test.dart b/test/fizzbuzz_test.dart
|
||||||
|
new file mode 100644
|
||||||
|
index 0000000..efeefe0
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/test/fizzbuzz_test.dart
|
||||||
|
@@ -0,0 +1,6 @@
|
||||||
|
+import 'package:test/test.dart';
|
||||||
|
+import '../lib/fizzbuzz.dart';
|
||||||
|
+
|
||||||
|
+void main() {
|
||||||
|
+ // Tests will be added step by step during the demo
|
||||||
|
+}
|
||||||
|
--
|
||||||
|
2.53.0
|
||||||
|
|
||||||
|
|
||||||
|
From 39357bebb6432b14ae45942da92de4804e747d02 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Dhemas Nurjaya <dhemasnurjaya@outlook.com>
|
||||||
|
Date: Tue, 10 Mar 2026 11:45:29 +0700
|
||||||
|
Subject: [PATCH 02/12] RED: Add test for input 1 returning "1"
|
||||||
|
|
||||||
|
---
|
||||||
|
test/fizzbuzz_test.dart | 4 +++-
|
||||||
|
1 file changed, 3 insertions(+), 1 deletion(-)
|
||||||
|
|
||||||
|
diff --git a/test/fizzbuzz_test.dart b/test/fizzbuzz_test.dart
|
||||||
|
index efeefe0..27e4266 100644
|
||||||
|
--- a/test/fizzbuzz_test.dart
|
||||||
|
+++ b/test/fizzbuzz_test.dart
|
||||||
|
@@ -2,5 +2,7 @@ import 'package:test/test.dart';
|
||||||
|
import '../lib/fizzbuzz.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
- // Tests will be added step by step during the demo
|
||||||
|
+ test('returns "1" for 1', () {
|
||||||
|
+ expect(fizzBuzz(1), equals('1'));
|
||||||
|
+ });
|
||||||
|
}
|
||||||
|
--
|
||||||
|
2.53.0
|
||||||
|
|
||||||
|
|
||||||
|
From 334c5b510611a36ccd7b5db02b3a7ad1e3ae0863 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Dhemas Nurjaya <dhemasnurjaya@outlook.com>
|
||||||
|
Date: Tue, 10 Mar 2026 11:45:41 +0700
|
||||||
|
Subject: [PATCH 03/12] GREEN: Return "1" (hardcoded minimal solution)
|
||||||
|
|
||||||
|
---
|
||||||
|
lib/fizzbuzz.dart | 6 +++++-
|
||||||
|
1 file changed, 5 insertions(+), 1 deletion(-)
|
||||||
|
|
||||||
|
diff --git a/lib/fizzbuzz.dart b/lib/fizzbuzz.dart
|
||||||
|
index 8c2215b..0178355 100644
|
||||||
|
--- a/lib/fizzbuzz.dart
|
||||||
|
+++ b/lib/fizzbuzz.dart
|
||||||
|
@@ -1 +1,5 @@
|
||||||
|
-// FizzBuzz kata - empty implementation file
|
||||||
|
+// FizzBuzz kata - TDD progression
|
||||||
|
+
|
||||||
|
+String fizzBuzz(int n) {
|
||||||
|
+ return '1';
|
||||||
|
+}
|
||||||
|
--
|
||||||
|
2.53.0
|
||||||
|
|
||||||
|
|
||||||
|
From e0ae220dea66522d1726e342820f7e2d61edaafb Mon Sep 17 00:00:00 2001
|
||||||
|
From: Dhemas Nurjaya <dhemasnurjaya@outlook.com>
|
||||||
|
Date: Tue, 10 Mar 2026 11:45:51 +0700
|
||||||
|
Subject: [PATCH 04/12] RED: Add test for input 2 returning "2"
|
||||||
|
|
||||||
|
---
|
||||||
|
test/fizzbuzz_test.dart | 4 ++++
|
||||||
|
1 file changed, 4 insertions(+)
|
||||||
|
|
||||||
|
diff --git a/test/fizzbuzz_test.dart b/test/fizzbuzz_test.dart
|
||||||
|
index 27e4266..7b517ae 100644
|
||||||
|
--- a/test/fizzbuzz_test.dart
|
||||||
|
+++ b/test/fizzbuzz_test.dart
|
||||||
|
@@ -5,4 +5,8 @@ void main() {
|
||||||
|
test('returns "1" for 1', () {
|
||||||
|
expect(fizzBuzz(1), equals('1'));
|
||||||
|
});
|
||||||
|
+
|
||||||
|
+ test('returns "2" for 2', () {
|
||||||
|
+ expect(fizzBuzz(2), equals('2'));
|
||||||
|
+ });
|
||||||
|
}
|
||||||
|
--
|
||||||
|
2.53.0
|
||||||
|
|
||||||
|
|
||||||
|
From 4c0471c02ac4bfef56517044519c65478ecd3dbd Mon Sep 17 00:00:00 2001
|
||||||
|
From: Dhemas Nurjaya <dhemasnurjaya@outlook.com>
|
||||||
|
Date: Tue, 10 Mar 2026 11:46:03 +0700
|
||||||
|
Subject: [PATCH 05/12] GREEN: Return string representation of number
|
||||||
|
|
||||||
|
---
|
||||||
|
lib/fizzbuzz.dart | 2 +-
|
||||||
|
1 file changed, 1 insertion(+), 1 deletion(-)
|
||||||
|
|
||||||
|
diff --git a/lib/fizzbuzz.dart b/lib/fizzbuzz.dart
|
||||||
|
index 0178355..4da1cfa 100644
|
||||||
|
--- a/lib/fizzbuzz.dart
|
||||||
|
+++ b/lib/fizzbuzz.dart
|
||||||
|
@@ -1,5 +1,5 @@
|
||||||
|
// FizzBuzz kata - TDD progression
|
||||||
|
|
||||||
|
String fizzBuzz(int n) {
|
||||||
|
- return '1';
|
||||||
|
+ return n.toString();
|
||||||
|
}
|
||||||
|
--
|
||||||
|
2.53.0
|
||||||
|
|
||||||
|
|
||||||
|
From 919e90fd474a2cc4200c9d6d0a0235365f6b718c Mon Sep 17 00:00:00 2001
|
||||||
|
From: Dhemas Nurjaya <dhemasnurjaya@outlook.com>
|
||||||
|
Date: Tue, 10 Mar 2026 11:46:14 +0700
|
||||||
|
Subject: [PATCH 06/12] RED: Add test for input 3 returning "Fizz"
|
||||||
|
|
||||||
|
---
|
||||||
|
test/fizzbuzz_test.dart | 4 ++++
|
||||||
|
1 file changed, 4 insertions(+)
|
||||||
|
|
||||||
|
diff --git a/test/fizzbuzz_test.dart b/test/fizzbuzz_test.dart
|
||||||
|
index 7b517ae..27b1ce6 100644
|
||||||
|
--- a/test/fizzbuzz_test.dart
|
||||||
|
+++ b/test/fizzbuzz_test.dart
|
||||||
|
@@ -9,4 +9,8 @@ void main() {
|
||||||
|
test('returns "2" for 2', () {
|
||||||
|
expect(fizzBuzz(2), equals('2'));
|
||||||
|
});
|
||||||
|
+
|
||||||
|
+ test('returns "Fizz" for 3', () {
|
||||||
|
+ expect(fizzBuzz(3), equals('Fizz'));
|
||||||
|
+ });
|
||||||
|
}
|
||||||
|
--
|
||||||
|
2.53.0
|
||||||
|
|
||||||
|
|
||||||
|
From 1db35633b102b359860a7e85361d733038e8655d Mon Sep 17 00:00:00 2001
|
||||||
|
From: Dhemas Nurjaya <dhemasnurjaya@outlook.com>
|
||||||
|
Date: Tue, 10 Mar 2026 11:46:27 +0700
|
||||||
|
Subject: [PATCH 07/12] GREEN: Return "Fizz" for numbers divisible by 3
|
||||||
|
|
||||||
|
---
|
||||||
|
lib/fizzbuzz.dart | 1 +
|
||||||
|
1 file changed, 1 insertion(+)
|
||||||
|
|
||||||
|
diff --git a/lib/fizzbuzz.dart b/lib/fizzbuzz.dart
|
||||||
|
index 4da1cfa..21150ec 100644
|
||||||
|
--- a/lib/fizzbuzz.dart
|
||||||
|
+++ b/lib/fizzbuzz.dart
|
||||||
|
@@ -1,5 +1,6 @@
|
||||||
|
// FizzBuzz kata - TDD progression
|
||||||
|
|
||||||
|
String fizzBuzz(int n) {
|
||||||
|
+ if (n % 3 == 0) return 'Fizz';
|
||||||
|
return n.toString();
|
||||||
|
}
|
||||||
|
--
|
||||||
|
2.53.0
|
||||||
|
|
||||||
|
|
||||||
|
From 08cc354e1f1798f062e9c947b7d64637870d8964 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Dhemas Nurjaya <dhemasnurjaya@outlook.com>
|
||||||
|
Date: Tue, 10 Mar 2026 11:46:38 +0700
|
||||||
|
Subject: [PATCH 08/12] RED: Add test for input 5 returning "Buzz"
|
||||||
|
|
||||||
|
---
|
||||||
|
test/fizzbuzz_test.dart | 4 ++++
|
||||||
|
1 file changed, 4 insertions(+)
|
||||||
|
|
||||||
|
diff --git a/test/fizzbuzz_test.dart b/test/fizzbuzz_test.dart
|
||||||
|
index 27b1ce6..46f279a 100644
|
||||||
|
--- a/test/fizzbuzz_test.dart
|
||||||
|
+++ b/test/fizzbuzz_test.dart
|
||||||
|
@@ -13,4 +13,8 @@ void main() {
|
||||||
|
test('returns "Fizz" for 3', () {
|
||||||
|
expect(fizzBuzz(3), equals('Fizz'));
|
||||||
|
});
|
||||||
|
+
|
||||||
|
+ test('returns "Buzz" for 5', () {
|
||||||
|
+ expect(fizzBuzz(5), equals('Buzz'));
|
||||||
|
+ });
|
||||||
|
}
|
||||||
|
--
|
||||||
|
2.53.0
|
||||||
|
|
||||||
|
|
||||||
|
From 74d6e23f3faa7c7138178ce00c784c51e97677e4 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Dhemas Nurjaya <dhemasnurjaya@outlook.com>
|
||||||
|
Date: Tue, 10 Mar 2026 11:46:52 +0700
|
||||||
|
Subject: [PATCH 09/12] GREEN: Return "Buzz" for numbers divisible by 5
|
||||||
|
|
||||||
|
---
|
||||||
|
lib/fizzbuzz.dart | 1 +
|
||||||
|
1 file changed, 1 insertion(+)
|
||||||
|
|
||||||
|
diff --git a/lib/fizzbuzz.dart b/lib/fizzbuzz.dart
|
||||||
|
index 21150ec..063e7bd 100644
|
||||||
|
--- a/lib/fizzbuzz.dart
|
||||||
|
+++ b/lib/fizzbuzz.dart
|
||||||
|
@@ -2,5 +2,6 @@
|
||||||
|
|
||||||
|
String fizzBuzz(int n) {
|
||||||
|
if (n % 3 == 0) return 'Fizz';
|
||||||
|
+ if (n % 5 == 0) return 'Buzz';
|
||||||
|
return n.toString();
|
||||||
|
}
|
||||||
|
--
|
||||||
|
2.53.0
|
||||||
|
|
||||||
|
|
||||||
|
From ca23fe7f17e7d29d8e375a4a2fc1ac32bf8452c6 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Dhemas Nurjaya <dhemasnurjaya@outlook.com>
|
||||||
|
Date: Tue, 10 Mar 2026 11:47:04 +0700
|
||||||
|
Subject: [PATCH 10/12] RED: Add test for input 15 returning "FizzBuzz"
|
||||||
|
|
||||||
|
---
|
||||||
|
test/fizzbuzz_test.dart | 4 ++++
|
||||||
|
1 file changed, 4 insertions(+)
|
||||||
|
|
||||||
|
diff --git a/test/fizzbuzz_test.dart b/test/fizzbuzz_test.dart
|
||||||
|
index 46f279a..f3203e3 100644
|
||||||
|
--- a/test/fizzbuzz_test.dart
|
||||||
|
+++ b/test/fizzbuzz_test.dart
|
||||||
|
@@ -17,4 +17,8 @@ void main() {
|
||||||
|
test('returns "Buzz" for 5', () {
|
||||||
|
expect(fizzBuzz(5), equals('Buzz'));
|
||||||
|
});
|
||||||
|
+
|
||||||
|
+ test('returns "FizzBuzz" for 15', () {
|
||||||
|
+ expect(fizzBuzz(15), equals('FizzBuzz'));
|
||||||
|
+ });
|
||||||
|
}
|
||||||
|
--
|
||||||
|
2.53.0
|
||||||
|
|
||||||
|
|
||||||
|
From f15c0989635612226d40e28c8507276ecd1c976d Mon Sep 17 00:00:00 2001
|
||||||
|
From: Dhemas Nurjaya <dhemasnurjaya@outlook.com>
|
||||||
|
Date: Tue, 10 Mar 2026 11:47:17 +0700
|
||||||
|
Subject: [PATCH 11/12] GREEN: Return "FizzBuzz" for numbers divisible by both
|
||||||
|
3 and 5 (check 15 first!)
|
||||||
|
|
||||||
|
---
|
||||||
|
lib/fizzbuzz.dart | 1 +
|
||||||
|
1 file changed, 1 insertion(+)
|
||||||
|
|
||||||
|
diff --git a/lib/fizzbuzz.dart b/lib/fizzbuzz.dart
|
||||||
|
index 063e7bd..b312695 100644
|
||||||
|
--- a/lib/fizzbuzz.dart
|
||||||
|
+++ b/lib/fizzbuzz.dart
|
||||||
|
@@ -1,6 +1,7 @@
|
||||||
|
// FizzBuzz kata - TDD progression
|
||||||
|
|
||||||
|
String fizzBuzz(int n) {
|
||||||
|
+ if (n % 15 == 0) return 'FizzBuzz';
|
||||||
|
if (n % 3 == 0) return 'Fizz';
|
||||||
|
if (n % 5 == 0) return 'Buzz';
|
||||||
|
return n.toString();
|
||||||
|
--
|
||||||
|
2.53.0
|
||||||
|
|
||||||
|
|
||||||
|
From cd13284513f428908ec0035f0848a12c27636eaa Mon Sep 17 00:00:00 2001
|
||||||
|
From: Dhemas Nurjaya <dhemasnurjaya@outlook.com>
|
||||||
|
Date: Tue, 10 Mar 2026 11:48:30 +0700
|
||||||
|
Subject: [PATCH 12/12] Add documentation for FizzBuzz demo kata
|
||||||
|
|
||||||
|
---
|
||||||
|
DEMO_SCRIPT.md | 346 +++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
README.md | 56 ++++++++
|
||||||
|
2 files changed, 402 insertions(+)
|
||||||
|
create mode 100644 DEMO_SCRIPT.md
|
||||||
|
create mode 100644 README.md
|
||||||
|
|
||||||
|
diff --git a/DEMO_SCRIPT.md b/DEMO_SCRIPT.md
|
||||||
|
new file mode 100644
|
||||||
|
index 0000000..d0853e0
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/DEMO_SCRIPT.md
|
||||||
|
@@ -0,0 +1,346 @@
|
||||||
|
+# FizzBuzz Live Demo Script
|
||||||
|
+
|
||||||
|
+**Duration:** 15 minutes (0:10–0:25 in workshop schedule)
|
||||||
|
+**Purpose:** Demonstrate RED-GREEN-REFACTOR cycle in real-time
|
||||||
|
+
|
||||||
|
+---
|
||||||
|
+
|
||||||
|
+## Setup (Before Demo Starts)
|
||||||
|
+
|
||||||
|
+- [ ] Open two windows side-by-side: `lib/fizzbuzz.dart` and `test/fizzbuzz_test.dart`
|
||||||
|
+- [ ] Have terminal ready at bottom with `dart test --watch` (optional) or be ready to run `dart test` manually
|
||||||
|
+- [ ] Start with the final completed kata, then `git reset --hard <initial-commit>` to beginning
|
||||||
|
+- [ ] Have git log open in another terminal: `git log --oneline --all`
|
||||||
|
+
|
||||||
|
+---
|
||||||
|
+
|
||||||
|
+## Demo Flow
|
||||||
|
+
|
||||||
|
+### Minutes 0-2: Introduction
|
||||||
|
+
|
||||||
|
+**What to Say:**
|
||||||
|
+```
|
||||||
|
+We're going to build FizzBuzz together using TDD. Everyone knows the rules, right?
|
||||||
|
+[Pause for nods]
|
||||||
|
+
|
||||||
|
+Divisible by 3 → "Fizz"
|
||||||
|
+Divisible by 5 → "Buzz"
|
||||||
|
+Divisible by both → "FizzBuzz"
|
||||||
|
+Otherwise → the number as a string
|
||||||
|
+
|
||||||
|
+Here's the key: I'm NOT going to design this first. I'm going to let the tests tell me what to code.
|
||||||
|
+
|
||||||
|
+Watch the rhythm:
|
||||||
|
+1. RED - Write a test, watch it fail
|
||||||
|
+2. GREEN - Write minimal code to pass
|
||||||
|
+3. REFACTOR - Clean up if needed
|
||||||
|
+
|
||||||
|
+Let's start.
|
||||||
|
+```
|
||||||
|
+
|
||||||
|
+**What to Do:**
|
||||||
|
+- Show the empty files
|
||||||
|
+- Run `dart test` → shows no tests or all passing (from setup)
|
||||||
|
+
|
||||||
|
+---
|
||||||
|
+
|
||||||
|
+### Minutes 2-4: Cycle 1 (Input 1 → "1")
|
||||||
|
+
|
||||||
|
+**RED Phase:**
|
||||||
|
+
|
||||||
|
+**What to Say:**
|
||||||
|
+```
|
||||||
|
+What's the simplest test I could write? Let me start with input 1 returning "1".
|
||||||
|
+```
|
||||||
|
+
|
||||||
|
+**What to Do:**
|
||||||
|
+```dart
|
||||||
|
+// In test/fizzbuzz_test.dart
|
||||||
|
+test('returns "1" for 1', () {
|
||||||
|
+ expect(fizzBuzz(1), equals('1'));
|
||||||
|
+});
|
||||||
|
+```
|
||||||
|
+
|
||||||
|
+Run `dart test` → **FAILS** (function doesn't exist)
|
||||||
|
+
|
||||||
|
+**What to Say:**
|
||||||
|
+```
|
||||||
|
+Good! It's RED. The test is failing for the right reason - the function doesn't exist yet.
|
||||||
|
+```
|
||||||
|
+
|
||||||
|
+**GREEN Phase:**
|
||||||
|
+
|
||||||
|
+**What to Say:**
|
||||||
|
+```
|
||||||
|
+Now, what's the minimum code to make this pass?
|
||||||
|
+[Pause, let them think]
|
||||||
|
+I could return the number... but even simpler: I'll just return "1".
|
||||||
|
+This looks silly, but watch what happens next.
|
||||||
|
+```
|
||||||
|
+
|
||||||
|
+**What to Do:**
|
||||||
|
+```dart
|
||||||
|
+// In lib/fizzbuzz.dart
|
||||||
|
+String fizzBuzz(int n) {
|
||||||
|
+ return '1';
|
||||||
|
+}
|
||||||
|
+```
|
||||||
|
+
|
||||||
|
+Run `dart test` → **PASSES** (GREEN ✅)
|
||||||
|
+
|
||||||
|
+**What to Say:**
|
||||||
|
+```
|
||||||
|
+GREEN! It passes. Now I could refactor, but there's nothing to clean up yet.
|
||||||
|
+Let's add another test.
|
||||||
|
+```
|
||||||
|
+
|
||||||
|
+**Time Check:** You should be at ~4 minutes
|
||||||
|
+
|
||||||
|
+---
|
||||||
|
+
|
||||||
|
+### Minutes 4-6: Cycle 2 (Input 2 → "2")
|
||||||
|
+
|
||||||
|
+**RED Phase:**
|
||||||
|
+
|
||||||
|
+**What to Say:**
|
||||||
|
+```
|
||||||
|
+Next simplest test: input 2 should return "2".
|
||||||
|
+```
|
||||||
|
+
|
||||||
|
+**What to Do:**
|
||||||
|
+```dart
|
||||||
|
+test('returns "2" for 2', () {
|
||||||
|
+ expect(fizzBuzz(2), equals('2'));
|
||||||
|
+});
|
||||||
|
+```
|
||||||
|
+
|
||||||
|
+Run `dart test` → **FAILS** (Expected: '2', Actual: '1')
|
||||||
|
+
|
||||||
|
+**What to Say:**
|
||||||
|
+```
|
||||||
|
+RED again. Now I have duplication in my tests - both expect numbers as strings.
|
||||||
|
+That's okay. Let me make it GREEN.
|
||||||
|
+```
|
||||||
|
+
|
||||||
|
+**GREEN Phase:**
|
||||||
|
+
|
||||||
|
+**What to Say:**
|
||||||
|
+```
|
||||||
|
+Now the hardcoded "1" won't work. What's the simplest solution?
|
||||||
|
+[Pause]
|
||||||
|
+Return the number as a string!
|
||||||
|
+```
|
||||||
|
+
|
||||||
|
+**What to Do:**
|
||||||
|
+```dart
|
||||||
|
+String fizzBuzz(int n) {
|
||||||
|
+ return n.toString();
|
||||||
|
+}
|
||||||
|
+```
|
||||||
|
+
|
||||||
|
+Run `dart test` → **PASSES** (both tests GREEN ✅)
|
||||||
|
+
|
||||||
|
+**What to Say:**
|
||||||
|
+```
|
||||||
|
+Both tests pass! Notice how the second test forced me to generalize.
|
||||||
|
+I didn't plan that - the tests guided me.
|
||||||
|
+```
|
||||||
|
+
|
||||||
|
+**Time Check:** You should be at ~6 minutes
|
||||||
|
+
|
||||||
|
+---
|
||||||
|
+
|
||||||
|
+### Minutes 6-9: Cycle 3 (Input 3 → "Fizz")
|
||||||
|
+
|
||||||
|
+**RED Phase:**
|
||||||
|
+
|
||||||
|
+**What to Say:**
|
||||||
|
+```
|
||||||
|
+Now it gets interesting. Input 3 should return "Fizz".
|
||||||
|
+```
|
||||||
|
+
|
||||||
|
+**What to Do:**
|
||||||
|
+```dart
|
||||||
|
+test('returns "Fizz" for 3', () {
|
||||||
|
+ expect(fizzBuzz(3), equals('Fizz'));
|
||||||
|
+});
|
||||||
|
+```
|
||||||
|
+
|
||||||
|
+Run `dart test` → **FAILS** (Expected: 'Fizz', Actual: '3')
|
||||||
|
+
|
||||||
|
+**GREEN Phase:**
|
||||||
|
+
|
||||||
|
+**What to Say:**
|
||||||
|
+```
|
||||||
|
+Okay, now I need to check divisibility by 3.
|
||||||
|
+```
|
||||||
|
+
|
||||||
|
+**What to Do:**
|
||||||
|
+```dart
|
||||||
|
+String fizzBuzz(int n) {
|
||||||
|
+ if (n % 3 == 0) return 'Fizz';
|
||||||
|
+ return n.toString();
|
||||||
|
+}
|
||||||
|
+```
|
||||||
|
+
|
||||||
|
+Run `dart test` → **PASSES** (all 3 tests GREEN ✅)
|
||||||
|
+
|
||||||
|
+**What to Say:**
|
||||||
|
+```
|
||||||
|
+All three pass! The algorithm is starting to emerge.
|
||||||
|
+```
|
||||||
|
+
|
||||||
|
+**Time Check:** You should be at ~9 minutes
|
||||||
|
+
|
||||||
|
+---
|
||||||
|
+
|
||||||
|
+### Minutes 9-11: Cycle 4 (Input 5 → "Buzz")
|
||||||
|
+
|
||||||
|
+**RED Phase:**
|
||||||
|
+
|
||||||
|
+**What to Say:**
|
||||||
|
+```
|
||||||
|
+Pattern should be clear now. Input 5 returns "Buzz".
|
||||||
|
+```
|
||||||
|
+
|
||||||
|
+**What to Do:**
|
||||||
|
+```dart
|
||||||
|
+test('returns "Buzz" for 5', () {
|
||||||
|
+ expect(fizzBuzz(5), equals('Buzz'));
|
||||||
|
+});
|
||||||
|
+```
|
||||||
|
+
|
||||||
|
+Run `dart test` → **FAILS** (Expected: 'Buzz', Actual: '5')
|
||||||
|
+
|
||||||
|
+**GREEN Phase:**
|
||||||
|
+
|
||||||
|
+**What to Do:**
|
||||||
|
+```dart
|
||||||
|
+String fizzBuzz(int n) {
|
||||||
|
+ if (n % 3 == 0) return 'Fizz';
|
||||||
|
+ if (n % 5 == 0) return 'Buzz';
|
||||||
|
+ return n.toString();
|
||||||
|
+}
|
||||||
|
+```
|
||||||
|
+
|
||||||
|
+Run `dart test` → **PASSES** (all 4 tests GREEN ✅)
|
||||||
|
+
|
||||||
|
+**What to Say:**
|
||||||
|
+```
|
||||||
|
+Four tests passing. But we haven't handled the tricky case yet...
|
||||||
|
+```
|
||||||
|
+
|
||||||
|
+---
|
||||||
|
+
|
||||||
|
+### Minutes 11-14: Cycle 5 (Input 15 → "FizzBuzz")
|
||||||
|
+
|
||||||
|
+**RED Phase:**
|
||||||
|
+
|
||||||
|
+**What to Say:**
|
||||||
|
+```
|
||||||
|
+Here's where it gets fun. What should 15 return?
|
||||||
|
+[Let them answer: "FizzBuzz"]
|
||||||
|
+Right! It's divisible by both 3 AND 5.
|
||||||
|
+```
|
||||||
|
+
|
||||||
|
+**What to Do:**
|
||||||
|
+```dart
|
||||||
|
+test('returns "FizzBuzz" for 15', () {
|
||||||
|
+ expect(fizzBuzz(15), equals('FizzBuzz'));
|
||||||
|
+});
|
||||||
|
+```
|
||||||
|
+
|
||||||
|
+Run `dart test` → **FAILS** (Expected: 'FizzBuzz', Actual: 'Fizz')
|
||||||
|
+
|
||||||
|
+**What to Say:**
|
||||||
|
+```
|
||||||
|
+Uh oh! It returns "Fizz" because 15 is divisible by 3, and we check that first.
|
||||||
|
+We need to check for BOTH before checking for either individually.
|
||||||
|
+```
|
||||||
|
+
|
||||||
|
+**GREEN Phase:**
|
||||||
|
+
|
||||||
|
+**What to Do:**
|
||||||
|
+```dart
|
||||||
|
+String fizzBuzz(int n) {
|
||||||
|
+ if (n % 15 == 0) return 'FizzBuzz'; // Or: if (n % 3 == 0 && n % 5 == 0)
|
||||||
|
+ if (n % 3 == 0) return 'Fizz';
|
||||||
|
+ if (n % 5 == 0) return 'Buzz';
|
||||||
|
+ return n.toString();
|
||||||
|
+}
|
||||||
|
+```
|
||||||
|
+
|
||||||
|
+Run `dart test` → **PASSES** (all 5 tests GREEN ✅)
|
||||||
|
+
|
||||||
|
+**What to Say:**
|
||||||
|
+```
|
||||||
|
+Perfect! All tests pass. ORDER MATTERS. That's a lesson the test taught us.
|
||||||
|
+```
|
||||||
|
+
|
||||||
|
+**Time Check:** You should be at ~14 minutes
|
||||||
|
+
|
||||||
|
+---
|
||||||
|
+
|
||||||
|
+### Minutes 14-15: Wrap Up
|
||||||
|
+
|
||||||
|
+**What to Say:**
|
||||||
|
+```
|
||||||
|
+Let me show you something cool.
|
||||||
|
+[Run: git log --oneline]
|
||||||
|
+
|
||||||
|
+See these commits? Each one is a RED or GREEN step. I can jump back to any point.
|
||||||
|
+
|
||||||
|
+Notice what just happened:
|
||||||
|
+1. I never wrote a "design document" for FizzBuzz
|
||||||
|
+2. The tests told me what code to write
|
||||||
|
+3. The algorithm emerged naturally from simple cases to complex
|
||||||
|
+4. When I made a mistake (checking 3 before 15), the test caught it immediately
|
||||||
|
+
|
||||||
|
+This is TDD. It feels weird at first, but it becomes automatic.
|
||||||
|
+
|
||||||
|
+Now YOU'RE going to try it. Pick your kata and start with the first test.
|
||||||
|
+```
|
||||||
|
+
|
||||||
|
+---
|
||||||
|
+
|
||||||
|
+## Tips for Smooth Delivery
|
||||||
|
+
|
||||||
|
+### DO:
|
||||||
|
+- **Narrate your thinking**: "What's the simplest test?" "What's the minimum code?"
|
||||||
|
+- **Pause before answering**: Let them think for 2-3 seconds
|
||||||
|
+- **Show RED clearly**: Let the test fail, read the error message aloud
|
||||||
|
+- **Type slowly**: They're watching and learning the syntax too
|
||||||
|
+- **Run tests frequently**: After every change
|
||||||
|
+
|
||||||
|
+### DON'T:
|
||||||
|
+- Rush through the cycles - the rhythm is what they're learning
|
||||||
|
+- Skip the RED step - it proves the test can fail
|
||||||
|
+- Write all tests at once - that's not TDD
|
||||||
|
+- Apologize for "simple" code - minimal solutions are the point
|
||||||
|
+- Skip explaining the 15/3/5 order issue - that's a key insight
|
||||||
|
+
|
||||||
|
+### If You Make a Typo:
|
||||||
|
+**Good!** Say: "Oops, typo. See how the test catches it?" Then fix it.
|
||||||
|
+
|
||||||
|
+### If Someone Asks a Question:
|
||||||
|
+Pause the demo, answer briefly, then continue. You have buffer time.
|
||||||
|
+
|
||||||
|
+---
|
||||||
|
+
|
||||||
|
+## Troubleshooting
|
||||||
|
+
|
||||||
|
+| Issue | Fix |
|
||||||
|
+|-------|-----|
|
||||||
|
+| Tests won't run | Check `dart pub get` was run |
|
||||||
|
+| Can't see failure messages clearly | Increase terminal font size |
|
||||||
|
+| Running behind schedule | Skip the "wrap up" or shorten it |
|
||||||
|
+| Running ahead | Add a 6th test (e.g., input 6 → "Fizz") |
|
||||||
|
+
|
||||||
|
+---
|
||||||
|
+
|
||||||
|
+## After the Demo
|
||||||
|
+
|
||||||
|
+Transition to hands-on:
|
||||||
|
+```
|
||||||
|
+"Alright, now you try it. Open one of the katas, uncomment the first test, and make it RED. Then make it GREEN. I'll be walking around - grab me if you get stuck. Go!"
|
||||||
|
+```
|
||||||
|
diff --git a/README.md b/README.md
|
||||||
|
new file mode 100644
|
||||||
|
index 0000000..c4ae816
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/README.md
|
||||||
|
@@ -0,0 +1,56 @@
|
||||||
|
+# FizzBuzz Kata - Live Demo
|
||||||
|
+
|
||||||
|
+## Purpose
|
||||||
|
+
|
||||||
|
+This kata is designed for **live demonstration** during the TDD workshop introduction. It shows the RED-GREEN-REFACTOR cycle in a familiar problem that requires minimal explanation.
|
||||||
|
+
|
||||||
|
+## The Rules
|
||||||
|
+
|
||||||
|
+Write a function that converts numbers to strings according to these rules:
|
||||||
|
+
|
||||||
|
+- Numbers divisible by 3 → `"Fizz"`
|
||||||
|
+- Numbers divisible by 5 → `"Buzz"`
|
||||||
|
+- Numbers divisible by both 3 and 5 → `"FizzBuzz"`
|
||||||
|
+- All other numbers → the number as a string (e.g., `"1"`, `"2"`)
|
||||||
|
+
|
||||||
|
+## Git History as Teaching Tool
|
||||||
|
+
|
||||||
|
+This kata is built with **12 commits** showing each step of the TDD cycle:
|
||||||
|
+
|
||||||
|
+```bash
|
||||||
|
+git log --oneline --all
|
||||||
|
+```
|
||||||
|
+
|
||||||
|
+Each commit shows either:
|
||||||
|
+- **RED**: A failing test
|
||||||
|
+- **GREEN**: Minimal code to pass
|
||||||
|
+- **REFACTOR**: Code cleanup (if needed)
|
||||||
|
+
|
||||||
|
+## Using This for the Demo
|
||||||
|
+
|
||||||
|
+See `DEMO_SCRIPT.md` for detailed talking points and timing guide.
|
||||||
|
+
|
||||||
|
+## Running the Tests
|
||||||
|
+
|
||||||
|
+```bash
|
||||||
|
+dart pub get
|
||||||
|
+dart test
|
||||||
|
+```
|
||||||
|
+
|
||||||
|
+## Checking Out Individual Steps
|
||||||
|
+
|
||||||
|
+To see the code at any point:
|
||||||
|
+
|
||||||
|
+```bash
|
||||||
|
+git log --oneline # See all commits
|
||||||
|
+git checkout <commit-hash> # Jump to that step
|
||||||
|
+git checkout main # Return to final solution
|
||||||
|
+```
|
||||||
|
+
|
||||||
|
+## The "Aha!" Moment
|
||||||
|
+
|
||||||
|
+Notice how the simple tests (1, 2) led to a general solution (n.toString()), and the Fizz/Buzz tests forced the algorithm to emerge naturally. **We never designed the whole solution upfront—the tests revealed it step by step.**
|
||||||
|
+
|
||||||
|
+## For Facilitators
|
||||||
|
+
|
||||||
|
+This is your warm-up kata. Practice it a few times before the workshop so you can live-code it smoothly while talking through your thinking process.
|
||||||
|
--
|
||||||
|
2.53.0
|
||||||
|
|
||||||
12
docs/fizzbuzz-git-log.txt
Normal file
12
docs/fizzbuzz-git-log.txt
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
cd13284513f428908ec0035f0848a12c27636eaa|Add documentation for FizzBuzz demo kata|Dhemas Nurjaya|dhemasnurjaya@outlook.com|2026-03-10 11:48:30 +0700
|
||||||
|
f15c0989635612226d40e28c8507276ecd1c976d|GREEN: Return "FizzBuzz" for numbers divisible by both 3 and 5 (check 15 first!)|Dhemas Nurjaya|dhemasnurjaya@outlook.com|2026-03-10 11:47:17 +0700
|
||||||
|
ca23fe7f17e7d29d8e375a4a2fc1ac32bf8452c6|RED: Add test for input 15 returning "FizzBuzz"|Dhemas Nurjaya|dhemasnurjaya@outlook.com|2026-03-10 11:47:04 +0700
|
||||||
|
74d6e23f3faa7c7138178ce00c784c51e97677e4|GREEN: Return "Buzz" for numbers divisible by 5|Dhemas Nurjaya|dhemasnurjaya@outlook.com|2026-03-10 11:46:52 +0700
|
||||||
|
08cc354e1f1798f062e9c947b7d64637870d8964|RED: Add test for input 5 returning "Buzz"|Dhemas Nurjaya|dhemasnurjaya@outlook.com|2026-03-10 11:46:38 +0700
|
||||||
|
1db35633b102b359860a7e85361d733038e8655d|GREEN: Return "Fizz" for numbers divisible by 3|Dhemas Nurjaya|dhemasnurjaya@outlook.com|2026-03-10 11:46:27 +0700
|
||||||
|
919e90fd474a2cc4200c9d6d0a0235365f6b718c|RED: Add test for input 3 returning "Fizz"|Dhemas Nurjaya|dhemasnurjaya@outlook.com|2026-03-10 11:46:14 +0700
|
||||||
|
4c0471c02ac4bfef56517044519c65478ecd3dbd|GREEN: Return string representation of number|Dhemas Nurjaya|dhemasnurjaya@outlook.com|2026-03-10 11:46:03 +0700
|
||||||
|
e0ae220dea66522d1726e342820f7e2d61edaafb|RED: Add test for input 2 returning "2"|Dhemas Nurjaya|dhemasnurjaya@outlook.com|2026-03-10 11:45:51 +0700
|
||||||
|
334c5b510611a36ccd7b5db02b3a7ad1e3ae0863|GREEN: Return "1" (hardcoded minimal solution)|Dhemas Nurjaya|dhemasnurjaya@outlook.com|2026-03-10 11:45:41 +0700
|
||||||
|
39357bebb6432b14ae45942da92de4804e747d02|RED: Add test for input 1 returning "1"|Dhemas Nurjaya|dhemasnurjaya@outlook.com|2026-03-10 11:45:29 +0700
|
||||||
|
71fbd5a6f30e2c7f6e5e49807cae18274bc44506|Initial setup: Create FizzBuzz kata structure|Dhemas Nurjaya|dhemasnurjaya@outlook.com|2026-03-10 11:44:55 +0700
|
||||||
199
docs/plans/2026-03-10-workshop-improvements-design.md
Normal file
199
docs/plans/2026-03-10-workshop-improvements-design.md
Normal file
|
|
@ -0,0 +1,199 @@
|
||||||
|
# TDD Workshop Improvements - Design Document
|
||||||
|
|
||||||
|
**Date:** 2026-03-10
|
||||||
|
**Author:** Workshop Facilitator
|
||||||
|
**Status:** Approved
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document outlines the design for enhancing the TDD workshop materials to create a comprehensive, ready-to-deliver 2-hour hands-on session for developers familiar with TDD concepts but lacking practical experience.
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
1. Provide a complete live-demo kata (FizzBuzz) with step-by-step progression
|
||||||
|
2. Enhance facilitator guidance with detailed scripts and timing
|
||||||
|
3. Create participant reference materials (TDD cheat sheet)
|
||||||
|
4. Add setup and troubleshooting documentation
|
||||||
|
5. Improve solution files with test-to-code traceability
|
||||||
|
|
||||||
|
## Target Audience
|
||||||
|
|
||||||
|
- **Participants:** Developers who know what TDD is but haven't practiced it regularly
|
||||||
|
- **Facilitators:** Workshop leaders who need clear guidance and scripts
|
||||||
|
- **Format:** 2-hour in-person workshop
|
||||||
|
- **Language:** Dart
|
||||||
|
|
||||||
|
## Design Decisions
|
||||||
|
|
||||||
|
### Approach: Incremental Enhancement
|
||||||
|
|
||||||
|
We'll build improvements in three phases:
|
||||||
|
|
||||||
|
1. **Phase 1:** Live demo foundation (FizzBuzz kata)
|
||||||
|
2. **Phase 2:** Documentation (guides, reference card, setup)
|
||||||
|
3. **Phase 3:** Polish (enhanced solutions, root README)
|
||||||
|
|
||||||
|
This allows for testing each component and ensures core materials are ready first.
|
||||||
|
|
||||||
|
### Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
tdd-workshop/
|
||||||
|
├── README.md # NEW: Workshop overview
|
||||||
|
├── FACILITATOR_GUIDE.md # ENHANCED: Add detailed scripts
|
||||||
|
├── WORKSHOP_PLAN.md # NEW: Complete schedule & checklists
|
||||||
|
├── TDD_REFERENCE_CARD.md # NEW: One-page participant handout
|
||||||
|
├── SETUP_GUIDE.md # NEW: Installation & troubleshooting
|
||||||
|
├── docs/
|
||||||
|
│ └── plans/ # Design documents
|
||||||
|
├── fizzbuzz/ # NEW: Live demo kata
|
||||||
|
│ ├── README.md
|
||||||
|
│ ├── DEMO_SCRIPT.md # Facilitator walkthrough
|
||||||
|
│ ├── pubspec.yaml
|
||||||
|
│ ├── lib/
|
||||||
|
│ │ └── fizzbuzz.dart
|
||||||
|
│ └── test/
|
||||||
|
│ └── fizzbuzz_test.dart
|
||||||
|
├── password_validator/
|
||||||
|
│ └── lib/
|
||||||
|
│ └── password_validator_solution.dart # ENHANCED: Add test references
|
||||||
|
└── shopping_cart/
|
||||||
|
└── lib/
|
||||||
|
└── shopping_cart_solution.dart # ENHANCED: Add test references
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Designs
|
||||||
|
|
||||||
|
### 1. FizzBuzz Demo Kata
|
||||||
|
|
||||||
|
**Purpose:** Provide a ready-to-demonstrate kata showing RED-GREEN-REFACTOR in real-time.
|
||||||
|
|
||||||
|
**Git Strategy:** Linear commit history showing each TDD cycle:
|
||||||
|
- Commits alternate between RED (failing test) and GREEN (minimal implementation)
|
||||||
|
- Each commit message clearly labeled: "RED: ...", "GREEN: ...", "REFACTOR: ..."
|
||||||
|
- Facilitator can `git checkout` each commit to show progression
|
||||||
|
|
||||||
|
**Key Commits:**
|
||||||
|
1. Initial setup
|
||||||
|
2. RED: Test for 1 → "1"
|
||||||
|
3. GREEN: Return "1" (hardcoded)
|
||||||
|
4. RED: Test for 2 → "2"
|
||||||
|
5. GREEN: Return n.toString()
|
||||||
|
6. RED: Test for 3 → "Fizz"
|
||||||
|
7. GREEN: Check divisibility by 3
|
||||||
|
8. RED: Test for 5 → "Buzz"
|
||||||
|
9. GREEN: Add divisibility by 5
|
||||||
|
10. RED: Test for 15 → "FizzBuzz"
|
||||||
|
11. GREEN: Check both (order matters!)
|
||||||
|
12. REFACTOR: Optional cleanup
|
||||||
|
|
||||||
|
**DEMO_SCRIPT.md Contents:**
|
||||||
|
- Minute-by-minute timing guide (0-5, 5-10, 10-15)
|
||||||
|
- Talking points for each step
|
||||||
|
- Common mistakes to demonstrate intentionally
|
||||||
|
- Key insights participants should notice
|
||||||
|
|
||||||
|
### 2. Enhanced Documentation
|
||||||
|
|
||||||
|
#### TDD_REFERENCE_CARD.md
|
||||||
|
One-page handout covering:
|
||||||
|
- RED-GREEN-REFACTOR cycle diagram
|
||||||
|
- When to refactor (code smells)
|
||||||
|
- Common test patterns (Arrange-Act-Assert)
|
||||||
|
- Dart test assertions quick reference
|
||||||
|
- Tips for staying disciplined
|
||||||
|
- Link to tdd-katas for more practice
|
||||||
|
|
||||||
|
#### SETUP_GUIDE.md
|
||||||
|
Pre-workshop setup with:
|
||||||
|
- Dart SDK prerequisites
|
||||||
|
- Installation steps
|
||||||
|
- Verification procedures
|
||||||
|
- Troubleshooting common issues (pub get fails, PATH issues, etc.)
|
||||||
|
- IDE setup recommendations
|
||||||
|
|
||||||
|
#### WORKSHOP_PLAN.md
|
||||||
|
Complete facilitator reference with:
|
||||||
|
- Pre-workshop checklist (1 week, 1 day, morning of)
|
||||||
|
- Minute-by-minute schedule (8 segments, 120 minutes)
|
||||||
|
- Materials checklist (digital & physical)
|
||||||
|
- Facilitation tips (energy management, handling questions)
|
||||||
|
- Success metrics
|
||||||
|
|
||||||
|
### 3. Enhanced FACILITATOR_GUIDE.md
|
||||||
|
|
||||||
|
**Additions:**
|
||||||
|
- Full scripts for each segment (not just bullet points)
|
||||||
|
- Detailed live demo section with FizzBuzz walkthrough
|
||||||
|
- Expanded intervention table with more scenarios
|
||||||
|
- Transition scripts between segments
|
||||||
|
- Backup plans for running ahead/behind
|
||||||
|
|
||||||
|
**Structure:**
|
||||||
|
- Each segment has "What to say", "What to do", "What success looks like"
|
||||||
|
- Timing checkpoints throughout
|
||||||
|
- Ready-to-use phrases for common situations
|
||||||
|
|
||||||
|
### 4. Enhanced Solution Files
|
||||||
|
|
||||||
|
**Strategy:** Add inline comments linking code to tests and explaining design evolution.
|
||||||
|
|
||||||
|
**Comment Types:**
|
||||||
|
- **Test reference:** `// STEP 2: From test "rejects password with no uppercase"`
|
||||||
|
- **Design decision:** `// Map chosen over List for O(1) lookup by name`
|
||||||
|
- **Refactoring note:** `// Originally 5 if-blocks → extracted to rule functions`
|
||||||
|
- **Pattern highlight:** `// This is the Open-Closed Principle in action`
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- Solutions become teaching tools
|
||||||
|
- Participants see why code looks this way
|
||||||
|
- Design patterns are explicitly named
|
||||||
|
|
||||||
|
### 5. Root README.md
|
||||||
|
|
||||||
|
**Purpose:** Orient anyone opening the repository.
|
||||||
|
|
||||||
|
**Sections:**
|
||||||
|
- Workshop overview (what, why, who)
|
||||||
|
- For facilitators (where to start)
|
||||||
|
- For participants (how to choose a kata)
|
||||||
|
- Repository structure explained
|
||||||
|
- Quick start instructions
|
||||||
|
- Links to additional resources
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
### Phase 1: FizzBuzz Kata (30-45 minutes)
|
||||||
|
1. Create fizzbuzz/ directory structure
|
||||||
|
2. Set up pubspec.yaml
|
||||||
|
3. Build kata with git commits (12 commits total)
|
||||||
|
4. Write DEMO_SCRIPT.md
|
||||||
|
5. Write fizzbuzz/README.md
|
||||||
|
|
||||||
|
### Phase 2: Documentation (45-60 minutes)
|
||||||
|
1. Create TDD_REFERENCE_CARD.md
|
||||||
|
2. Create SETUP_GUIDE.md
|
||||||
|
3. Create WORKSHOP_PLAN.md (incorporate detailed schedule)
|
||||||
|
4. Enhance FACILITATOR_GUIDE.md with scripts
|
||||||
|
|
||||||
|
### Phase 3: Polish (30-45 minutes)
|
||||||
|
1. Enhance password_validator_solution.dart with comments
|
||||||
|
2. Enhance shopping_cart_solution.dart with comments
|
||||||
|
3. Create root README.md
|
||||||
|
4. Review all materials for consistency
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
- [ ] Facilitator can run the entire 2-hour workshop using only these materials
|
||||||
|
- [ ] Live demo kata is ready to present without preparation
|
||||||
|
- [ ] Participants have clear setup instructions and reference materials
|
||||||
|
- [ ] Solution files explain design decisions, not just implementation
|
||||||
|
- [ ] All documentation uses consistent tone and terminology
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
None - design is approved and ready for implementation.
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
Proceed with implementation using the three-phase approach outlined above.
|
||||||
346
fizzbuzz/DEMO_SCRIPT.md
Normal file
346
fizzbuzz/DEMO_SCRIPT.md
Normal file
|
|
@ -0,0 +1,346 @@
|
||||||
|
# FizzBuzz Live Demo Script
|
||||||
|
|
||||||
|
**Duration:** 15 minutes (0:10–0:25 in workshop schedule)
|
||||||
|
**Purpose:** Demonstrate RED-GREEN-REFACTOR cycle in real-time
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Setup (Before Demo Starts)
|
||||||
|
|
||||||
|
- [ ] Open two windows side-by-side: `lib/fizzbuzz.dart` and `test/fizzbuzz_test.dart`
|
||||||
|
- [ ] Have terminal ready at bottom with `dart test --watch` (optional) or be ready to run `dart test` manually
|
||||||
|
- [ ] Start with the final completed kata, then `git reset --hard <initial-commit>` to beginning
|
||||||
|
- [ ] Have git log open in another terminal: `git log --oneline --all`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Demo Flow
|
||||||
|
|
||||||
|
### Minutes 0-2: Introduction
|
||||||
|
|
||||||
|
**What to Say:**
|
||||||
|
```
|
||||||
|
We're going to build FizzBuzz together using TDD. Everyone knows the rules, right?
|
||||||
|
[Pause for nods]
|
||||||
|
|
||||||
|
Divisible by 3 → "Fizz"
|
||||||
|
Divisible by 5 → "Buzz"
|
||||||
|
Divisible by both → "FizzBuzz"
|
||||||
|
Otherwise → the number as a string
|
||||||
|
|
||||||
|
Here's the key: I'm NOT going to design this first. I'm going to let the tests tell me what to code.
|
||||||
|
|
||||||
|
Watch the rhythm:
|
||||||
|
1. RED - Write a test, watch it fail
|
||||||
|
2. GREEN - Write minimal code to pass
|
||||||
|
3. REFACTOR - Clean up if needed
|
||||||
|
|
||||||
|
Let's start.
|
||||||
|
```
|
||||||
|
|
||||||
|
**What to Do:**
|
||||||
|
- Show the empty files
|
||||||
|
- Run `dart test` → shows no tests or all passing (from setup)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Minutes 2-4: Cycle 1 (Input 1 → "1")
|
||||||
|
|
||||||
|
**RED Phase:**
|
||||||
|
|
||||||
|
**What to Say:**
|
||||||
|
```
|
||||||
|
What's the simplest test I could write? Let me start with input 1 returning "1".
|
||||||
|
```
|
||||||
|
|
||||||
|
**What to Do:**
|
||||||
|
```dart
|
||||||
|
// In test/fizzbuzz_test.dart
|
||||||
|
test('returns "1" for 1', () {
|
||||||
|
expect(fizzBuzz(1), equals('1'));
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Run `dart test` → **FAILS** (function doesn't exist)
|
||||||
|
|
||||||
|
**What to Say:**
|
||||||
|
```
|
||||||
|
Good! It's RED. The test is failing for the right reason - the function doesn't exist yet.
|
||||||
|
```
|
||||||
|
|
||||||
|
**GREEN Phase:**
|
||||||
|
|
||||||
|
**What to Say:**
|
||||||
|
```
|
||||||
|
Now, what's the minimum code to make this pass?
|
||||||
|
[Pause, let them think]
|
||||||
|
I could return the number... but even simpler: I'll just return "1".
|
||||||
|
This looks silly, but watch what happens next.
|
||||||
|
```
|
||||||
|
|
||||||
|
**What to Do:**
|
||||||
|
```dart
|
||||||
|
// In lib/fizzbuzz.dart
|
||||||
|
String fizzBuzz(int n) {
|
||||||
|
return '1';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Run `dart test` → **PASSES** (GREEN ✅)
|
||||||
|
|
||||||
|
**What to Say:**
|
||||||
|
```
|
||||||
|
GREEN! It passes. Now I could refactor, but there's nothing to clean up yet.
|
||||||
|
Let's add another test.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Time Check:** You should be at ~4 minutes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Minutes 4-6: Cycle 2 (Input 2 → "2")
|
||||||
|
|
||||||
|
**RED Phase:**
|
||||||
|
|
||||||
|
**What to Say:**
|
||||||
|
```
|
||||||
|
Next simplest test: input 2 should return "2".
|
||||||
|
```
|
||||||
|
|
||||||
|
**What to Do:**
|
||||||
|
```dart
|
||||||
|
test('returns "2" for 2', () {
|
||||||
|
expect(fizzBuzz(2), equals('2'));
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Run `dart test` → **FAILS** (Expected: '2', Actual: '1')
|
||||||
|
|
||||||
|
**What to Say:**
|
||||||
|
```
|
||||||
|
RED again. Now I have duplication in my tests - both expect numbers as strings.
|
||||||
|
That's okay. Let me make it GREEN.
|
||||||
|
```
|
||||||
|
|
||||||
|
**GREEN Phase:**
|
||||||
|
|
||||||
|
**What to Say:**
|
||||||
|
```
|
||||||
|
Now the hardcoded "1" won't work. What's the simplest solution?
|
||||||
|
[Pause]
|
||||||
|
Return the number as a string!
|
||||||
|
```
|
||||||
|
|
||||||
|
**What to Do:**
|
||||||
|
```dart
|
||||||
|
String fizzBuzz(int n) {
|
||||||
|
return n.toString();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Run `dart test` → **PASSES** (both tests GREEN ✅)
|
||||||
|
|
||||||
|
**What to Say:**
|
||||||
|
```
|
||||||
|
Both tests pass! Notice how the second test forced me to generalize.
|
||||||
|
I didn't plan that - the tests guided me.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Time Check:** You should be at ~6 minutes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Minutes 6-9: Cycle 3 (Input 3 → "Fizz")
|
||||||
|
|
||||||
|
**RED Phase:**
|
||||||
|
|
||||||
|
**What to Say:**
|
||||||
|
```
|
||||||
|
Now it gets interesting. Input 3 should return "Fizz".
|
||||||
|
```
|
||||||
|
|
||||||
|
**What to Do:**
|
||||||
|
```dart
|
||||||
|
test('returns "Fizz" for 3', () {
|
||||||
|
expect(fizzBuzz(3), equals('Fizz'));
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Run `dart test` → **FAILS** (Expected: 'Fizz', Actual: '3')
|
||||||
|
|
||||||
|
**GREEN Phase:**
|
||||||
|
|
||||||
|
**What to Say:**
|
||||||
|
```
|
||||||
|
Okay, now I need to check divisibility by 3.
|
||||||
|
```
|
||||||
|
|
||||||
|
**What to Do:**
|
||||||
|
```dart
|
||||||
|
String fizzBuzz(int n) {
|
||||||
|
if (n % 3 == 0) return 'Fizz';
|
||||||
|
return n.toString();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Run `dart test` → **PASSES** (all 3 tests GREEN ✅)
|
||||||
|
|
||||||
|
**What to Say:**
|
||||||
|
```
|
||||||
|
All three pass! The algorithm is starting to emerge.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Time Check:** You should be at ~9 minutes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Minutes 9-11: Cycle 4 (Input 5 → "Buzz")
|
||||||
|
|
||||||
|
**RED Phase:**
|
||||||
|
|
||||||
|
**What to Say:**
|
||||||
|
```
|
||||||
|
Pattern should be clear now. Input 5 returns "Buzz".
|
||||||
|
```
|
||||||
|
|
||||||
|
**What to Do:**
|
||||||
|
```dart
|
||||||
|
test('returns "Buzz" for 5', () {
|
||||||
|
expect(fizzBuzz(5), equals('Buzz'));
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Run `dart test` → **FAILS** (Expected: 'Buzz', Actual: '5')
|
||||||
|
|
||||||
|
**GREEN Phase:**
|
||||||
|
|
||||||
|
**What to Do:**
|
||||||
|
```dart
|
||||||
|
String fizzBuzz(int n) {
|
||||||
|
if (n % 3 == 0) return 'Fizz';
|
||||||
|
if (n % 5 == 0) return 'Buzz';
|
||||||
|
return n.toString();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Run `dart test` → **PASSES** (all 4 tests GREEN ✅)
|
||||||
|
|
||||||
|
**What to Say:**
|
||||||
|
```
|
||||||
|
Four tests passing. But we haven't handled the tricky case yet...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Minutes 11-14: Cycle 5 (Input 15 → "FizzBuzz")
|
||||||
|
|
||||||
|
**RED Phase:**
|
||||||
|
|
||||||
|
**What to Say:**
|
||||||
|
```
|
||||||
|
Here's where it gets fun. What should 15 return?
|
||||||
|
[Let them answer: "FizzBuzz"]
|
||||||
|
Right! It's divisible by both 3 AND 5.
|
||||||
|
```
|
||||||
|
|
||||||
|
**What to Do:**
|
||||||
|
```dart
|
||||||
|
test('returns "FizzBuzz" for 15', () {
|
||||||
|
expect(fizzBuzz(15), equals('FizzBuzz'));
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Run `dart test` → **FAILS** (Expected: 'FizzBuzz', Actual: 'Fizz')
|
||||||
|
|
||||||
|
**What to Say:**
|
||||||
|
```
|
||||||
|
Uh oh! It returns "Fizz" because 15 is divisible by 3, and we check that first.
|
||||||
|
We need to check for BOTH before checking for either individually.
|
||||||
|
```
|
||||||
|
|
||||||
|
**GREEN Phase:**
|
||||||
|
|
||||||
|
**What to Do:**
|
||||||
|
```dart
|
||||||
|
String fizzBuzz(int n) {
|
||||||
|
if (n % 15 == 0) return 'FizzBuzz'; // Or: if (n % 3 == 0 && n % 5 == 0)
|
||||||
|
if (n % 3 == 0) return 'Fizz';
|
||||||
|
if (n % 5 == 0) return 'Buzz';
|
||||||
|
return n.toString();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Run `dart test` → **PASSES** (all 5 tests GREEN ✅)
|
||||||
|
|
||||||
|
**What to Say:**
|
||||||
|
```
|
||||||
|
Perfect! All tests pass. ORDER MATTERS. That's a lesson the test taught us.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Time Check:** You should be at ~14 minutes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Minutes 14-15: Wrap Up
|
||||||
|
|
||||||
|
**What to Say:**
|
||||||
|
```
|
||||||
|
Let me show you something cool.
|
||||||
|
[Run: git log --oneline]
|
||||||
|
|
||||||
|
See these commits? Each one is a RED or GREEN step. I can jump back to any point.
|
||||||
|
|
||||||
|
Notice what just happened:
|
||||||
|
1. I never wrote a "design document" for FizzBuzz
|
||||||
|
2. The tests told me what code to write
|
||||||
|
3. The algorithm emerged naturally from simple cases to complex
|
||||||
|
4. When I made a mistake (checking 3 before 15), the test caught it immediately
|
||||||
|
|
||||||
|
This is TDD. It feels weird at first, but it becomes automatic.
|
||||||
|
|
||||||
|
Now YOU'RE going to try it. Pick your kata and start with the first test.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tips for Smooth Delivery
|
||||||
|
|
||||||
|
### DO:
|
||||||
|
- **Narrate your thinking**: "What's the simplest test?" "What's the minimum code?"
|
||||||
|
- **Pause before answering**: Let them think for 2-3 seconds
|
||||||
|
- **Show RED clearly**: Let the test fail, read the error message aloud
|
||||||
|
- **Type slowly**: They're watching and learning the syntax too
|
||||||
|
- **Run tests frequently**: After every change
|
||||||
|
|
||||||
|
### DON'T:
|
||||||
|
- Rush through the cycles - the rhythm is what they're learning
|
||||||
|
- Skip the RED step - it proves the test can fail
|
||||||
|
- Write all tests at once - that's not TDD
|
||||||
|
- Apologize for "simple" code - minimal solutions are the point
|
||||||
|
- Skip explaining the 15/3/5 order issue - that's a key insight
|
||||||
|
|
||||||
|
### If You Make a Typo:
|
||||||
|
**Good!** Say: "Oops, typo. See how the test catches it?" Then fix it.
|
||||||
|
|
||||||
|
### If Someone Asks a Question:
|
||||||
|
Pause the demo, answer briefly, then continue. You have buffer time.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Issue | Fix |
|
||||||
|
|-------|-----|
|
||||||
|
| Tests won't run | Check `dart pub get` was run |
|
||||||
|
| Can't see failure messages clearly | Increase terminal font size |
|
||||||
|
| Running behind schedule | Skip the "wrap up" or shorten it |
|
||||||
|
| Running ahead | Add a 6th test (e.g., input 6 → "Fizz") |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## After the Demo
|
||||||
|
|
||||||
|
Transition to hands-on:
|
||||||
|
```
|
||||||
|
"Alright, now you try it. Open one of the katas, uncomment the first test, and make it RED. Then make it GREEN. I'll be walking around - grab me if you get stuck. Go!"
|
||||||
|
```
|
||||||
56
fizzbuzz/README.md
Normal file
56
fizzbuzz/README.md
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
# FizzBuzz Kata - Live Demo
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This kata is designed for **live demonstration** during the TDD workshop introduction. It shows the RED-GREEN-REFACTOR cycle in a familiar problem that requires minimal explanation.
|
||||||
|
|
||||||
|
## The Rules
|
||||||
|
|
||||||
|
Write a function that converts numbers to strings according to these rules:
|
||||||
|
|
||||||
|
- Numbers divisible by 3 → `"Fizz"`
|
||||||
|
- Numbers divisible by 5 → `"Buzz"`
|
||||||
|
- Numbers divisible by both 3 and 5 → `"FizzBuzz"`
|
||||||
|
- All other numbers → the number as a string (e.g., `"1"`, `"2"`)
|
||||||
|
|
||||||
|
## Git History as Teaching Tool
|
||||||
|
|
||||||
|
This kata is built with **12 commits** showing each step of the TDD cycle:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git log --oneline --all
|
||||||
|
```
|
||||||
|
|
||||||
|
Each commit shows either:
|
||||||
|
- **RED**: A failing test
|
||||||
|
- **GREEN**: Minimal code to pass
|
||||||
|
- **REFACTOR**: Code cleanup (if needed)
|
||||||
|
|
||||||
|
## Using This for the Demo
|
||||||
|
|
||||||
|
See `DEMO_SCRIPT.md` for detailed talking points and timing guide.
|
||||||
|
|
||||||
|
## Running the Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dart pub get
|
||||||
|
dart test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Checking Out Individual Steps
|
||||||
|
|
||||||
|
To see the code at any point:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git log --oneline # See all commits
|
||||||
|
git checkout <commit-hash> # Jump to that step
|
||||||
|
git checkout main # Return to final solution
|
||||||
|
```
|
||||||
|
|
||||||
|
## The "Aha!" Moment
|
||||||
|
|
||||||
|
Notice how the simple tests (1, 2) led to a general solution (n.toString()), and the Fizz/Buzz tests forced the algorithm to emerge naturally. **We never designed the whole solution upfront—the tests revealed it step by step.**
|
||||||
|
|
||||||
|
## For Facilitators
|
||||||
|
|
||||||
|
This is your warm-up kata. Practice it a few times before the workshop so you can live-code it smoothly while talking through your thinking process.
|
||||||
8
fizzbuzz/lib/fizzbuzz.dart
Normal file
8
fizzbuzz/lib/fizzbuzz.dart
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
// FizzBuzz kata - TDD progression
|
||||||
|
|
||||||
|
String fizzBuzz(int n) {
|
||||||
|
if (n % 15 == 0) return 'FizzBuzz';
|
||||||
|
if (n % 3 == 0) return 'Fizz';
|
||||||
|
if (n % 5 == 0) return 'Buzz';
|
||||||
|
return n.toString();
|
||||||
|
}
|
||||||
9
fizzbuzz/pubspec.yaml
Normal file
9
fizzbuzz/pubspec.yaml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
name: fizzbuzz
|
||||||
|
description: FizzBuzz kata for TDD workshop live demonstration
|
||||||
|
version: 1.0.0
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ^3.0.0
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
test: ^1.24.0
|
||||||
24
fizzbuzz/test/fizzbuzz_test.dart
Normal file
24
fizzbuzz/test/fizzbuzz_test.dart
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
import '../lib/fizzbuzz.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('returns "1" for 1', () {
|
||||||
|
expect(fizzBuzz(1), equals('1'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns "2" for 2', () {
|
||||||
|
expect(fizzBuzz(2), equals('2'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns "Fizz" for 3', () {
|
||||||
|
expect(fizzBuzz(3), equals('Fizz'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns "Buzz" for 5', () {
|
||||||
|
expect(fizzBuzz(5), equals('Buzz'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns "FizzBuzz" for 15', () {
|
||||||
|
expect(fizzBuzz(15), equals('FizzBuzz'));
|
||||||
|
});
|
||||||
|
}
|
||||||
372
password_validator/DEMO_SCRIPT.md
Normal file
372
password_validator/DEMO_SCRIPT.md
Normal file
|
|
@ -0,0 +1,372 @@
|
||||||
|
# Password Validator — Demo Script for Facilitators
|
||||||
|
|
||||||
|
**Purpose:** This script helps you demonstrate the Password Validator kata if participants get stuck, or for debriefing discussions. Unlike the FizzBuzz live demo, this is NOT meant to be performed in real-time. Use it as a reference guide.
|
||||||
|
|
||||||
|
**Target time:** 45 minutes (participant hands-on)
|
||||||
|
**Your role:** Circulate, answer questions, refer to this script when helping stuck participants
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview of the Kata
|
||||||
|
|
||||||
|
**Validation rules to implement (in order):**
|
||||||
|
|
||||||
|
1. At least 8 characters long
|
||||||
|
2. At least one uppercase letter (A–Z)
|
||||||
|
3. At least one digit (0–9)
|
||||||
|
4. At least one special character (!@#$%^&*)
|
||||||
|
5. No spaces
|
||||||
|
6. **Refactor:** All errors reported at once
|
||||||
|
|
||||||
|
**Key learning goals:**
|
||||||
|
- Practice RED-GREEN-REFACTOR rhythm
|
||||||
|
- Experience code evolution through tests
|
||||||
|
- Discover the Open-Closed Principle naturally (Step 6)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Suggested Test Order & Implementation
|
||||||
|
|
||||||
|
### STEP 1: Minimum Length (Start Simple)
|
||||||
|
|
||||||
|
**First test to uncomment (line 23):**
|
||||||
|
```dart
|
||||||
|
test('rejects a password shorter than 8 characters', () {
|
||||||
|
final result = validator.validate('Ab1!xyz');
|
||||||
|
expect(result.isValid, isFalse);
|
||||||
|
expect(result.errors, contains('Must be at least 8 characters long'));
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected failure:**
|
||||||
|
- "The method 'validate' isn't defined for the class 'PasswordValidator'"
|
||||||
|
|
||||||
|
**Minimum code to make it GREEN:**
|
||||||
|
```dart
|
||||||
|
class ValidationResult {
|
||||||
|
final bool isValid;
|
||||||
|
final List<String> errors;
|
||||||
|
|
||||||
|
const ValidationResult({required this.isValid, required this.errors});
|
||||||
|
}
|
||||||
|
|
||||||
|
class PasswordValidator {
|
||||||
|
ValidationResult validate(String password) {
|
||||||
|
if (password.length < 8) {
|
||||||
|
return ValidationResult(
|
||||||
|
isValid: false,
|
||||||
|
errors: ['Must be at least 8 characters long']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return ValidationResult(isValid: true, errors: []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Next tests (lines 30, 36):**
|
||||||
|
- "accepts exactly 8 characters" → already passes!
|
||||||
|
- "accepts longer than 8 characters" → already passes!
|
||||||
|
|
||||||
|
**Teaching moment:** "Notice how the general solution covered the edge cases automatically."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### STEP 2: Uppercase Letter Required
|
||||||
|
|
||||||
|
**Test to uncomment (line 48):**
|
||||||
|
```dart
|
||||||
|
test('rejects a password with no uppercase letters', () {
|
||||||
|
final result = validator.validate('ab1!xyzw');
|
||||||
|
expect(result.isValid, isFalse);
|
||||||
|
expect(result.errors, contains('Must contain at least one uppercase letter'));
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected failure:**
|
||||||
|
- Test fails because 'ab1!xyzw' is 8 chars, so it passes validation
|
||||||
|
|
||||||
|
**Minimum code to make it GREEN:**
|
||||||
|
```dart
|
||||||
|
ValidationResult validate(String password) {
|
||||||
|
final errors = <String>[];
|
||||||
|
|
||||||
|
if (password.length < 8) {
|
||||||
|
errors.add('Must be at least 8 characters long');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!password.contains(RegExp(r'[A-Z]'))) {
|
||||||
|
errors.add('Must contain at least one uppercase letter');
|
||||||
|
}
|
||||||
|
|
||||||
|
return ValidationResult(
|
||||||
|
isValid: errors.isEmpty,
|
||||||
|
errors: errors,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Teaching moment:** "We switched from single error to a list. This is the design emerging from the tests."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### STEP 3: Digit Required
|
||||||
|
|
||||||
|
**Test to uncomment (line 67):**
|
||||||
|
```dart
|
||||||
|
test('rejects a password with no digits', () {
|
||||||
|
final result = validator.validate('Ab!!xyzw');
|
||||||
|
expect(result.isValid, isFalse);
|
||||||
|
expect(result.errors, contains('Must contain at least one digit'));
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Code to add:**
|
||||||
|
```dart
|
||||||
|
if (!password.contains(RegExp(r'[0-9]'))) {
|
||||||
|
errors.add('Must contain at least one digit');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Teaching moment:** "Notice the pattern? Each rule is just another if-block. We'll refactor this later."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### STEP 4: Special Character Required
|
||||||
|
|
||||||
|
**Test to uncomment (line 86):**
|
||||||
|
```dart
|
||||||
|
test('rejects a password with no special characters', () {
|
||||||
|
final result = validator.validate('Ab1xxxyw');
|
||||||
|
expect(result.isValid, isFalse);
|
||||||
|
expect(result.errors, contains('Must contain at least one special character (!@#\$%^&*)'));
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Code to add:**
|
||||||
|
```dart
|
||||||
|
if (!password.contains(RegExp(r'[!@#$%^&*]'))) {
|
||||||
|
errors.add(r'Must contain at least one special character (!@#$%^&*)');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### STEP 5: No Spaces Allowed
|
||||||
|
|
||||||
|
**Test to uncomment (line 104):**
|
||||||
|
```dart
|
||||||
|
test('rejects a password that contains a space', () {
|
||||||
|
final result = validator.validate('Ab1! xyz');
|
||||||
|
expect(result.isValid, isFalse);
|
||||||
|
expect(result.errors, contains('Must not contain spaces'));
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Code to add:**
|
||||||
|
```dart
|
||||||
|
if (password.contains(' ')) {
|
||||||
|
errors.add('Must not contain spaces');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**At this point, all tests pass! ✅ But the code has duplication...**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### STEP 6: Refactor to Rule Functions (The Insight)
|
||||||
|
|
||||||
|
**Test to uncomment (line 124):**
|
||||||
|
```dart
|
||||||
|
test('reports all failures for a completely invalid password', () {
|
||||||
|
final result = validator.validate('bad');
|
||||||
|
expect(result.isValid, isFalse);
|
||||||
|
expect(result.errors.length, equals(4)); // length, uppercase, digit, special char
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**This test already passes!** But now we refactor for maintainability.
|
||||||
|
|
||||||
|
**Before refactoring, the validate() method looks like:**
|
||||||
|
```dart
|
||||||
|
ValidationResult validate(String password) {
|
||||||
|
final errors = <String>[];
|
||||||
|
if (password.length < 8) errors.add('Must be at least 8 characters long');
|
||||||
|
if (!password.contains(RegExp(r'[A-Z]'))) errors.add('Must contain at least one uppercase letter');
|
||||||
|
if (!password.contains(RegExp(r'[0-9]'))) errors.add('Must contain at least one digit');
|
||||||
|
if (!password.contains(RegExp(r'[!@#$%^&*]'))) errors.add(r'Must contain at least one special character (!@#$%^&*)');
|
||||||
|
if (password.contains(' ')) errors.add('Must not contain spaces');
|
||||||
|
return ValidationResult(isValid: errors.isEmpty, errors: errors);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problem:** Adding a 6th rule means modifying this method. What if we had 20 rules?
|
||||||
|
|
||||||
|
**Refactoring insight:** Extract each rule into its own function.
|
||||||
|
|
||||||
|
**After refactoring:**
|
||||||
|
```dart
|
||||||
|
typedef PasswordRule = String? Function(String password);
|
||||||
|
|
||||||
|
class PasswordValidator {
|
||||||
|
static final List<PasswordRule> _rules = [
|
||||||
|
_minimumLength,
|
||||||
|
_requiresUppercase,
|
||||||
|
_requiresDigit,
|
||||||
|
_requiresSpecialCharacter,
|
||||||
|
_noSpaces,
|
||||||
|
];
|
||||||
|
|
||||||
|
ValidationResult validate(String password) {
|
||||||
|
final errors = _rules
|
||||||
|
.map((rule) => rule(password))
|
||||||
|
.whereType<String>()
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return ValidationResult(
|
||||||
|
isValid: errors.isEmpty,
|
||||||
|
errors: errors,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String? _minimumLength(String password) {
|
||||||
|
if (password.length < 8) {
|
||||||
|
return 'Must be at least 8 characters long';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String? _requiresUppercase(String password) {
|
||||||
|
if (!password.contains(RegExp(r'[A-Z]'))) {
|
||||||
|
return 'Must contain at least one uppercase letter';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String? _requiresDigit(String password) {
|
||||||
|
if (!password.contains(RegExp(r'[0-9]'))) {
|
||||||
|
return 'Must contain at least one digit';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String? _requiresSpecialCharacter(String password) {
|
||||||
|
if (!password.contains(RegExp(r'[!@#$%^&*]'))) {
|
||||||
|
return r'Must contain at least one special character (!@#$%^&*)';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String? _noSpaces(String password) {
|
||||||
|
if (password.contains(' ')) {
|
||||||
|
return 'Must not contain spaces';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Run tests again → Still GREEN! ✅**
|
||||||
|
|
||||||
|
**Teaching moment:** "This is the Open-Closed Principle in action:
|
||||||
|
- **Open for extension:** Add a new rule? Just add it to the `_rules` list.
|
||||||
|
- **Closed for modification:** The `validate()` method never needs to change.
|
||||||
|
|
||||||
|
This refactoring was safe because the tests stayed green throughout!"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Participant Questions
|
||||||
|
|
||||||
|
### "Why return null instead of false?"
|
||||||
|
|
||||||
|
**Answer:** Returning `null` means "no error" while returning a String means "here's the error message." This pattern is common in Dart (nullable types) and makes the code more expressive than true/false.
|
||||||
|
|
||||||
|
### "Can I combine all the RegExp patterns into one big pattern?"
|
||||||
|
|
||||||
|
**Answer:** You could, but then you lose granular error messages. Try it and see what happens to the tests! (They'll fail because you can't report specific errors.)
|
||||||
|
|
||||||
|
### "Why use static methods for rules?"
|
||||||
|
|
||||||
|
**Answer:** Rules don't need instance state—they just take a password and return an error. Static methods make this clear. You could also use top-level functions.
|
||||||
|
|
||||||
|
### "Should I test the individual rule functions?"
|
||||||
|
|
||||||
|
**Answer:** Not necessary in this kata—they're private implementation details. The public `validate()` method is fully tested through its behavior. However, in real production code, you might extract rules to separate files and test them individually.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Debrief Discussion Points (5-10 min after kata)
|
||||||
|
|
||||||
|
Ask participants:
|
||||||
|
|
||||||
|
1. **"At what point did you notice the duplication?"**
|
||||||
|
- Most notice it around Step 3-4
|
||||||
|
- Some push through to Step 5 before refactoring
|
||||||
|
|
||||||
|
2. **"What made you decide to refactor?"**
|
||||||
|
- Listen for: "too much repetition," "hard to add new rules," "messy code"
|
||||||
|
- Reinforce: Tests gave you confidence to refactor safely
|
||||||
|
|
||||||
|
3. **"How did the tests help during refactoring?"**
|
||||||
|
- They stayed green → proving the behavior didn't change
|
||||||
|
- Could experiment freely knowing tests would catch mistakes
|
||||||
|
|
||||||
|
4. **"What's the benefit of the rule functions approach?"**
|
||||||
|
- Easier to add new rules (just add to list)
|
||||||
|
- Each rule is isolated and easier to understand
|
||||||
|
- Could even load rules from configuration!
|
||||||
|
|
||||||
|
5. **"Did anyone try a different approach?"**
|
||||||
|
- Some might use class-based rules, strategy pattern, etc.
|
||||||
|
- All valid! TDD doesn't prescribe the design, just guides it
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Timing Guidance
|
||||||
|
|
||||||
|
**For 45-minute practice session:**
|
||||||
|
- 0-5 min: Participants read README, set up files
|
||||||
|
- 5-25 min: Implement Steps 1-5 (about 4 min per rule)
|
||||||
|
- 25-35 min: Refactor Step 6
|
||||||
|
- 35-40 min: Bonus edge cases (if time)
|
||||||
|
- 40-45 min: Quick debrief
|
||||||
|
|
||||||
|
**If participants finish early:**
|
||||||
|
- Direct them to bonus edge cases in the tests
|
||||||
|
- Challenge: "Can you add a 6th rule without modifying validate()?"
|
||||||
|
- Suggest: "Try extracting rules to a separate file"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Participant stuck on first test
|
||||||
|
- Check: Did they create `ValidationResult` class?
|
||||||
|
- Check: Did they create `validate()` method?
|
||||||
|
- Hint: "Start with the absolute minimum to make it compile and fail correctly"
|
||||||
|
|
||||||
|
### Tests not running
|
||||||
|
- Check: Did they run `dart pub get`?
|
||||||
|
- Check: Are they in the `password_validator` directory?
|
||||||
|
- Check: Did they uncomment the test code?
|
||||||
|
|
||||||
|
### Participant trying to write all code at once
|
||||||
|
- Remind: "Just make *this one test* pass. Nothing more."
|
||||||
|
- Ask: "What's the simplest code that could work?"
|
||||||
|
|
||||||
|
### Participant stuck on refactoring
|
||||||
|
- Ask: "What pattern do you see repeated?"
|
||||||
|
- Hint: "What if each rule was a function?"
|
||||||
|
- Show: The typedef example from the solution
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
This kata demonstrates:
|
||||||
|
- ✅ The RED-GREEN-REFACTOR rhythm in practice
|
||||||
|
- ✅ How tests enable safe refactoring
|
||||||
|
- ✅ The Open-Closed Principle emerging naturally from TDD
|
||||||
|
- ✅ Design evolution driven by tests, not upfront planning
|
||||||
|
|
||||||
|
**Key facilitator role:** Encourage participants to feel the pain of duplication *before* refactoring. The refactoring is more meaningful when they discover it themselves.
|
||||||
43
password_validator/README.md
Normal file
43
password_validator/README.md
Normal file
|
|
@ -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?
|
||||||
25
password_validator/lib/password_validator.dart
Normal file
25
password_validator/lib/password_validator.dart
Normal file
|
|
@ -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<String> errors;
|
||||||
|
|
||||||
|
const ValidationResult({required this.isValid, required this.errors});
|
||||||
|
}
|
||||||
|
|
||||||
|
class PasswordValidator {
|
||||||
|
ValidationResult validate(String password) {
|
||||||
|
// TODO: implement
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
127
password_validator/lib/password_validator_solution.dart
Normal file
127
password_validator/lib/password_validator_solution.dart
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
// REFERENCE SOLUTION — For facilitator use only.
|
||||||
|
// Do not share with participants before the session.
|
||||||
|
//
|
||||||
|
// This solution shows the evolution of the code through TDD:
|
||||||
|
// - Steps 1-5: Individual rules implemented as separate if-blocks
|
||||||
|
// - Step 6: Refactored to use rule functions (Open-Closed Principle)
|
||||||
|
|
||||||
|
class ValidationResult {
|
||||||
|
final bool isValid;
|
||||||
|
final List<String> errors;
|
||||||
|
|
||||||
|
const ValidationResult({required this.isValid, required this.errors});
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP 6 REFACTOR: This typedef emerged after implementing all 5 rules
|
||||||
|
// It represents the pattern: "A rule takes a password and returns an error message or null"
|
||||||
|
// Defining this makes the refactoring much cleaner
|
||||||
|
typedef PasswordRule = String? Function(String password);
|
||||||
|
|
||||||
|
class PasswordValidator {
|
||||||
|
// STEP 6 REFACTOR: Originally had 5 separate if-blocks in validate()
|
||||||
|
// Tests revealed the duplication - each rule followed the same pattern
|
||||||
|
// Extracted each rule into its own function and collected them in a list
|
||||||
|
// This is the Open-Closed Principle: open for extension (add new rule to list),
|
||||||
|
// closed for modification (validate() method never changes)
|
||||||
|
static final List<PasswordRule> _rules = [
|
||||||
|
_minimumLength, // STEP 1
|
||||||
|
_requiresUppercase, // STEP 2
|
||||||
|
_requiresDigit, // STEP 3
|
||||||
|
_requiresSpecialCharacter, // STEP 4
|
||||||
|
_noSpaces, // STEP 5
|
||||||
|
];
|
||||||
|
|
||||||
|
ValidationResult validate(String password) {
|
||||||
|
// STEP 6 REFACTOR: This elegant implementation emerged from refactoring
|
||||||
|
// It runs all rules, filters out null (passing rules), and collects errors
|
||||||
|
// Before refactoring, this was 20+ lines of nested if-statements
|
||||||
|
final errors = _rules
|
||||||
|
.map((rule) => rule(password))
|
||||||
|
.whereType<String>() // Filters out nulls (rules that passed)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return ValidationResult(
|
||||||
|
isValid: errors.isEmpty,
|
||||||
|
errors: errors,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP 1: From test "rejects password shorter than 8 characters"
|
||||||
|
// See: test/password_validator_test.dart:23
|
||||||
|
// This was the first rule implemented - started simple
|
||||||
|
static String? _minimumLength(String password) {
|
||||||
|
if (password.length < 8) {
|
||||||
|
return 'Must be at least 8 characters long';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP 2: From test "rejects password with no uppercase letters"
|
||||||
|
// See: test/password_validator_test.dart:48
|
||||||
|
// Used RegExp to check for at least one A-Z character
|
||||||
|
static String? _requiresUppercase(String password) {
|
||||||
|
if (!password.contains(RegExp(r'[A-Z]'))) {
|
||||||
|
return 'Must contain at least one uppercase letter';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP 3: From test "rejects password with no digits"
|
||||||
|
// See: test/password_validator_test.dart:67
|
||||||
|
// Pattern similar to uppercase - started to see duplication here
|
||||||
|
static String? _requiresDigit(String password) {
|
||||||
|
if (!password.contains(RegExp(r'[0-9]'))) {
|
||||||
|
return 'Must contain at least one digit';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP 4: From test "rejects password with no special characters"
|
||||||
|
// See: test/password_validator_test.dart:86
|
||||||
|
// RegExp gets more specific - only certain special chars allowed
|
||||||
|
static String? _requiresSpecialCharacter(String password) {
|
||||||
|
if (!password.contains(RegExp(r'[!@#$%^&*]'))) {
|
||||||
|
return r'Must contain at least one special character (!@#$%^&*)';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP 5: From test "rejects password that contains a space"
|
||||||
|
// See: test/password_validator_test.dart:104
|
||||||
|
// Simplest check - just look for space character
|
||||||
|
static String? _noSpaces(String password) {
|
||||||
|
if (password.contains(' ')) {
|
||||||
|
return 'Must not contain spaces';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EVOLUTION NOTES:
|
||||||
|
//
|
||||||
|
// Initial Implementation (Steps 1-5):
|
||||||
|
// The validate() method looked like this:
|
||||||
|
//
|
||||||
|
// ValidationResult validate(String password) {
|
||||||
|
// final errors = <String>[];
|
||||||
|
// if (password.length < 8) errors.add('Must be at least 8 characters long');
|
||||||
|
// if (!password.contains(RegExp(r'[A-Z]'))) errors.add('Must contain at least one uppercase letter');
|
||||||
|
// if (!password.contains(RegExp(r'[0-9]'))) errors.add('Must contain at least one digit');
|
||||||
|
// if (!password.contains(RegExp(r'[!@#$%^&*]'))) errors.add(r'Must contain at least one special character (!@#$%^&*)');
|
||||||
|
// if (password.contains(' ')) errors.add('Must not contain spaces');
|
||||||
|
// return ValidationResult(isValid: errors.isEmpty, errors: errors);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// This worked! All tests passed. But...
|
||||||
|
//
|
||||||
|
// REFACTORING INSIGHT (Step 6):
|
||||||
|
// - Each rule follows the same pattern: check condition, add error if fails
|
||||||
|
// - Adding a 6th rule means modifying validate() method (violates Open-Closed)
|
||||||
|
// - What if we had 20 rules? 50 rules? This becomes unreadable
|
||||||
|
//
|
||||||
|
// Solution: Extract each rule into its own function
|
||||||
|
// - Each function is independent and testable
|
||||||
|
// - Adding new rule = adding one line to _rules list
|
||||||
|
// - validate() method never needs to change
|
||||||
|
//
|
||||||
|
// This refactoring was driven by the tests - they stayed GREEN throughout!
|
||||||
8
password_validator/pubspec.yaml
Normal file
8
password_validator/pubspec.yaml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
name: password_validator
|
||||||
|
description: TDD Workshop - Feature A
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ^3.0.0
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
test: ^1.25.0
|
||||||
157
password_validator/test/password_validator_test.dart
Normal file
157
password_validator/test/password_validator_test.dart
Normal file
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
477
shopping_cart/DEMO_SCRIPT.md
Normal file
477
shopping_cart/DEMO_SCRIPT.md
Normal file
|
|
@ -0,0 +1,477 @@
|
||||||
|
# Shopping Cart — Demo Script for Facilitators
|
||||||
|
|
||||||
|
**Purpose:** This script helps you demonstrate the Shopping Cart kata if participants get stuck, or for debriefing discussions. Unlike the FizzBuzz live demo, this is NOT meant to be performed in real-time. Use it as a reference guide.
|
||||||
|
|
||||||
|
**Target time:** 45 minutes (participant hands-on)
|
||||||
|
**Your role:** Circulate, answer questions, refer to this script when helping stuck participants
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview of the Kata
|
||||||
|
|
||||||
|
**Features to implement (in order):**
|
||||||
|
|
||||||
|
1. Empty cart has subtotal 0 and count 0
|
||||||
|
2. Add items — subtotal = price × quantity
|
||||||
|
3. Remove item by name
|
||||||
|
4. Apply % discount to subtotal
|
||||||
|
5. CartItem rejects zero/negative price & quantity (Value Object)
|
||||||
|
6. **Bonus:** Duplicate name handling / float precision
|
||||||
|
|
||||||
|
**Key learning goals:**
|
||||||
|
- Practice RED-GREEN-REFACTOR rhythm
|
||||||
|
- Let tests drive design decisions (List → Map refactoring)
|
||||||
|
- Discover Value Object pattern naturally (Step 5)
|
||||||
|
- Experience design evolution through failing tests
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Suggested Test Order & Implementation
|
||||||
|
|
||||||
|
### STEP 1: Empty Cart (The Baseline)
|
||||||
|
|
||||||
|
**First test to uncomment (line 23):**
|
||||||
|
```dart
|
||||||
|
test('subtotal is zero when cart is empty', () {
|
||||||
|
expect(cart.subtotal(), equals(0.0));
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected failure:**
|
||||||
|
- "The method 'subtotal' isn't defined for the class 'ShoppingCart'"
|
||||||
|
|
||||||
|
**Minimum code to make it GREEN:**
|
||||||
|
```dart
|
||||||
|
class ShoppingCart {
|
||||||
|
double subtotal() {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Next test (line 28):**
|
||||||
|
```dart
|
||||||
|
test('item count is zero when cart is empty', () {
|
||||||
|
expect(cart.itemCount(), equals(0));
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Code to add:**
|
||||||
|
```dart
|
||||||
|
int itemCount() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Teaching moment:** "Always start with the simplest case. Empty state is your baseline."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### STEP 2: Adding Items
|
||||||
|
|
||||||
|
**Test to uncomment (line 39):**
|
||||||
|
```dart
|
||||||
|
test('subtotal reflects a single item added', () {
|
||||||
|
cart.addItem(CartItem(name: 'Apple', price: 1.50, quantity: 3));
|
||||||
|
expect(cart.subtotal(), equals(4.50));
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected failure:**
|
||||||
|
- "The class 'CartItem' isn't defined"
|
||||||
|
- "The method 'addItem' isn't defined"
|
||||||
|
|
||||||
|
**Minimum code to make it GREEN:**
|
||||||
|
```dart
|
||||||
|
class CartItem {
|
||||||
|
final String name;
|
||||||
|
final double price;
|
||||||
|
final int quantity;
|
||||||
|
|
||||||
|
CartItem({required this.name, required this.price, required this.quantity});
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShoppingCart {
|
||||||
|
final List<CartItem> _items = [];
|
||||||
|
|
||||||
|
void addItem(CartItem item) {
|
||||||
|
_items.add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
double subtotal() {
|
||||||
|
if (_items.isEmpty) return 0.0;
|
||||||
|
return _items.first.price * _items.first.quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
int itemCount() {
|
||||||
|
if (_items.isEmpty) return 0;
|
||||||
|
return _items.first.quantity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Teaching moment:** "This is intentionally naive! We only handle the first item. The next tests will force us to generalize."
|
||||||
|
|
||||||
|
**Next test (line 51):**
|
||||||
|
```dart
|
||||||
|
test('subtotal accumulates across multiple different items', () {
|
||||||
|
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));
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected failure:**
|
||||||
|
- Test fails because `subtotal()` only looks at first item
|
||||||
|
|
||||||
|
**Code to fix:**
|
||||||
|
```dart
|
||||||
|
double subtotal() {
|
||||||
|
return _items.fold(0.0, (sum, item) => sum + (item.price * item.quantity));
|
||||||
|
}
|
||||||
|
|
||||||
|
int itemCount() {
|
||||||
|
return _items.fold(0, (sum, item) => sum + item.quantity);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Teaching moment:** "The test forced us to handle multiple items. We went from naive to general."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### STEP 3: Removing Items (The Design Pivot)
|
||||||
|
|
||||||
|
**Test to uncomment (line 71):**
|
||||||
|
```dart
|
||||||
|
test('removing an item reduces the subtotal', () {
|
||||||
|
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));
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**First attempt (works but feels wrong):**
|
||||||
|
```dart
|
||||||
|
void removeItem(String name) {
|
||||||
|
_items.removeWhere((item) => item.name == name);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**This passes the tests! But... do we like it?**
|
||||||
|
|
||||||
|
**Teaching moment:** "This works, but we're doing an O(n) search every time. The test is telling us something: we're removing *by name*, not by position. What data structure is optimized for name lookups?"
|
||||||
|
|
||||||
|
**Refactoring opportunity:**
|
||||||
|
```dart
|
||||||
|
class ShoppingCart {
|
||||||
|
// Changed from List to Map!
|
||||||
|
final Map<String, CartItem> _items = {};
|
||||||
|
|
||||||
|
void addItem(CartItem item) {
|
||||||
|
_items[item.name] = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeItem(String name) {
|
||||||
|
_items.remove(name); // Much simpler!
|
||||||
|
}
|
||||||
|
|
||||||
|
double subtotal() {
|
||||||
|
return _items.values.fold(0.0, (sum, item) => sum + (item.price * item.quantity));
|
||||||
|
}
|
||||||
|
|
||||||
|
int itemCount() {
|
||||||
|
return _items.values.fold(0, (sum, item) => sum + item.quantity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Run tests → Still GREEN! ✅**
|
||||||
|
|
||||||
|
**Teaching moment:** "This is TDD at work:
|
||||||
|
1. The tests gave us confidence to refactor
|
||||||
|
2. The failing test (removeItem) revealed the better design
|
||||||
|
3. Map makes the code simpler AND more efficient
|
||||||
|
4. We discovered this through tests, not upfront planning"
|
||||||
|
|
||||||
|
**Next test (line 87):**
|
||||||
|
```dart
|
||||||
|
test('removing a non-existent item does nothing', () {
|
||||||
|
cart.addItem(CartItem(name: 'Apple', price: 1.50, quantity: 2));
|
||||||
|
cart.removeItem('Orange'); // not in cart
|
||||||
|
expect(cart.subtotal(), equals(3.00));
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**This already passes!** Map.remove() silently handles missing keys.
|
||||||
|
|
||||||
|
**Teaching moment:** "Our refactoring gave us this behavior for free. Good design often does."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### STEP 4: Discount Calculation
|
||||||
|
|
||||||
|
**Test to uncomment (line 109):**
|
||||||
|
```dart
|
||||||
|
test('10% discount reduces subtotal correctly', () {
|
||||||
|
cart.addItem(CartItem(name: 'Apple', price: 10.00, quantity: 5)); // 50.00
|
||||||
|
expect(cart.totalAfterDiscount(10), equals(45.00));
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected failure:**
|
||||||
|
- "The method 'totalAfterDiscount' isn't defined"
|
||||||
|
|
||||||
|
**Minimum code to make it GREEN:**
|
||||||
|
```dart
|
||||||
|
double totalAfterDiscount(double discountPercent) {
|
||||||
|
final discount = discountPercent / 100;
|
||||||
|
return subtotal() * (1 - discount);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Next tests (lines 115, 121, 127):**
|
||||||
|
- "0% discount returns full subtotal" → passes!
|
||||||
|
- "100% discount results in zero" → passes!
|
||||||
|
- "discount on empty cart returns zero" → passes!
|
||||||
|
|
||||||
|
**Teaching moment:** "Notice how we got edge cases for free by implementing the general formula correctly."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### STEP 5: Value Object Validation (The Design Pattern)
|
||||||
|
|
||||||
|
**Test to uncomment (line 139):**
|
||||||
|
```dart
|
||||||
|
test('rejects an item with a zero price', () {
|
||||||
|
expect(
|
||||||
|
() => CartItem(name: 'Free thing', price: 0, quantity: 1),
|
||||||
|
throwsArgumentError,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**First attempt (common mistake):**
|
||||||
|
|
||||||
|
Many participants will add validation to `addItem()`:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
void addItem(CartItem item) {
|
||||||
|
if (item.price <= 0) {
|
||||||
|
throw ArgumentError('Price must be greater than zero');
|
||||||
|
}
|
||||||
|
_items[item.name] = item;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**This makes the test pass, BUT...**
|
||||||
|
|
||||||
|
**Ask the participant:** "What if CartItem is created somewhere else in the code? Can it exist with a zero price?"
|
||||||
|
|
||||||
|
**The answer:** Yes! And that's a problem.
|
||||||
|
|
||||||
|
**Better approach - move validation to CartItem constructor:**
|
||||||
|
```dart
|
||||||
|
class CartItem {
|
||||||
|
final String name;
|
||||||
|
final double price;
|
||||||
|
final int quantity;
|
||||||
|
|
||||||
|
CartItem({required this.name, required this.price, required this.quantity}) {
|
||||||
|
if (price <= 0) {
|
||||||
|
throw ArgumentError('Price must be greater than zero, got: $price');
|
||||||
|
}
|
||||||
|
if (quantity <= 0) {
|
||||||
|
throw ArgumentError('Quantity must be greater than zero, got: $quantity');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Now remove validation from addItem():**
|
||||||
|
```dart
|
||||||
|
void addItem(CartItem item) {
|
||||||
|
_items[item.name] = item; // Simple again!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Run tests → All GREEN! ✅**
|
||||||
|
|
||||||
|
**Teaching moment:** "This is the Value Object pattern:
|
||||||
|
- **Immutable data** (final fields)
|
||||||
|
- **Validation at construction** (in constructor)
|
||||||
|
- **Invalid state is impossible** (can't create bad CartItem)
|
||||||
|
- **No need to revalidate** (if it exists, it's valid)
|
||||||
|
|
||||||
|
Where does validation belong? As close to the data as possible. Not in the cart, but in the item itself."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BONUS: Edge Cases (If Time Permits)
|
||||||
|
|
||||||
|
**Duplicate names test (line 168):**
|
||||||
|
|
||||||
|
This test is commented with a design choice - participants must decide:
|
||||||
|
|
||||||
|
**Option A: Replace** (current implementation)
|
||||||
|
```dart
|
||||||
|
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, not added
|
||||||
|
```
|
||||||
|
|
||||||
|
Using `_items[item.name] = item` naturally replaces duplicates.
|
||||||
|
|
||||||
|
**Option B: Accumulate**
|
||||||
|
```dart
|
||||||
|
cart.addItem(CartItem(name: 'Apple', price: 1.50, quantity: 2));
|
||||||
|
cart.addItem(CartItem(name: 'Apple', price: 1.50, quantity: 5));
|
||||||
|
expect(cart.itemCount(), equals(7)); // quantities stacked
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires changing `addItem()`:
|
||||||
|
```dart
|
||||||
|
void addItem(CartItem item) {
|
||||||
|
if (_items.containsKey(item.name)) {
|
||||||
|
final existing = _items[item.name]!;
|
||||||
|
_items[item.name] = CartItem(
|
||||||
|
name: item.name,
|
||||||
|
price: item.price,
|
||||||
|
quantity: existing.quantity + item.quantity,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_items[item.name] = item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Teaching moment:** "Both are valid! TDD doesn't prescribe the design—you do. The test just documents your decision."
|
||||||
|
|
||||||
|
**Float precision test (line 181):**
|
||||||
|
```dart
|
||||||
|
test('subtotal handles floating point prices accurately', () {
|
||||||
|
cart.addItem(CartItem(name: 'Item', price: 0.1, quantity: 3));
|
||||||
|
expect(cart.subtotal(), closeTo(0.30, 0.001)); // use closeTo!
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
This demonstrates floating-point arithmetic issues (0.1 + 0.1 + 0.1 ≠ 0.3 exactly).
|
||||||
|
|
||||||
|
**Teaching moment:** "Always use `closeTo()` for float comparisons in tests. This is a fundamental programming lesson!"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Participant Questions
|
||||||
|
|
||||||
|
### "Why use a Map instead of a List?"
|
||||||
|
|
||||||
|
**Answer:** The `removeItem(name)` test revealed this. With a List, you have to search by name every time (O(n)). With a Map, removal is O(1). The test drove us toward the better data structure.
|
||||||
|
|
||||||
|
### "Should I validate in CartItem or in ShoppingCart?"
|
||||||
|
|
||||||
|
**Answer:** CartItem. Ask yourself: "Can an invalid CartItem exist?" If yes, you can't trust it anywhere in your code. Value Objects enforce validity at creation time.
|
||||||
|
|
||||||
|
### "What if I want to accumulate quantities for duplicate names?"
|
||||||
|
|
||||||
|
**Answer:** That's a valid design choice! Write a test that documents that behavior, then implement it. TDD lets you decide—just make the decision explicit through tests.
|
||||||
|
|
||||||
|
### "Can I add a method to list all items?"
|
||||||
|
|
||||||
|
**Answer:** Do you have a failing test for it? If not, you're writing production code without a test (YAGNI - You Aren't Gonna Need It). Write the test first!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Debrief Discussion Points (5-10 min after kata)
|
||||||
|
|
||||||
|
Ask participants:
|
||||||
|
|
||||||
|
1. **"When did you realize List wasn't the right choice?"**
|
||||||
|
- Most realize during Step 3 (removeItem)
|
||||||
|
- Some continue with List and never refactor (ask them why!)
|
||||||
|
|
||||||
|
2. **"What made you switch from List to Map?"**
|
||||||
|
- Listen for: "removeItem by name," "felt inefficient," "Map is cleaner"
|
||||||
|
- Reinforce: The test drove the design decision
|
||||||
|
|
||||||
|
3. **"Where did you put the validation initially?"**
|
||||||
|
- Many start in `addItem()`, then move to `CartItem`
|
||||||
|
- Ask: "What changed your mind?"
|
||||||
|
|
||||||
|
4. **"What's the benefit of validating in the constructor?"**
|
||||||
|
- Invalid state is impossible
|
||||||
|
- No need to check validity elsewhere
|
||||||
|
- Confidence that any CartItem is valid
|
||||||
|
|
||||||
|
5. **"How did the tests help during refactoring?"**
|
||||||
|
- Confidence to change from List to Map
|
||||||
|
- Proof that behavior didn't break
|
||||||
|
- Freedom to experiment
|
||||||
|
|
||||||
|
6. **"What design patterns emerged?"**
|
||||||
|
- Value Object (CartItem validation)
|
||||||
|
- Collection wrapper (ShoppingCart wrapping Map)
|
||||||
|
- Functional operations (fold, map, values)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Timing Guidance
|
||||||
|
|
||||||
|
**For 45-minute practice session:**
|
||||||
|
- 0-5 min: Participants read README, set up files
|
||||||
|
- 5-15 min: Steps 1-2 (Empty cart, Adding items)
|
||||||
|
- 15-25 min: Step 3 (Removing items, List → Map refactor)
|
||||||
|
- 25-30 min: Step 4 (Discount calculation)
|
||||||
|
- 30-40 min: Step 5 (Value Object validation)
|
||||||
|
- 40-45 min: Bonus edge cases (if time)
|
||||||
|
|
||||||
|
**If participants finish early:**
|
||||||
|
- Direct them to bonus tests
|
||||||
|
- Challenge: "Add a `clear()` method using TDD"
|
||||||
|
- Challenge: "Add a `getItems()` method that returns a read-only view"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Participant stuck on first test
|
||||||
|
- Check: Did they create `subtotal()` method?
|
||||||
|
- Hint: "What's the simplest value you can return?"
|
||||||
|
|
||||||
|
### Still using List for removeItem
|
||||||
|
- Ask: "How does removeWhere work internally?"
|
||||||
|
- Ask: "What if you had 10,000 items?"
|
||||||
|
- Hint: "What data structure is optimized for name lookups?"
|
||||||
|
|
||||||
|
### Validation in addItem instead of CartItem
|
||||||
|
- Ask: "Can a CartItem be created outside of addItem?"
|
||||||
|
- Ask: "If yes, can it have a negative price?"
|
||||||
|
- Hint: "Where can you guarantee the item is ALWAYS valid?"
|
||||||
|
|
||||||
|
### Tests not running
|
||||||
|
- Check: Did they run `dart pub get`?
|
||||||
|
- Check: Are they in the `shopping_cart` directory?
|
||||||
|
- Check: Did they uncomment the test code?
|
||||||
|
|
||||||
|
### Float precision issues
|
||||||
|
- Show: `0.1 + 0.1 + 0.1` in Dart REPL
|
||||||
|
- Explain: Floating point representation
|
||||||
|
- Solution: Use `closeTo(expected, delta)` in tests
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
This kata demonstrates:
|
||||||
|
- ✅ Tests driving design decisions (List → Map)
|
||||||
|
- ✅ Value Object pattern emerging from validation needs
|
||||||
|
- ✅ Safe refactoring with test coverage
|
||||||
|
- ✅ Design evolution through incremental testing
|
||||||
|
- ✅ The power of "make it work, then make it right"
|
||||||
|
|
||||||
|
**Key facilitator role:**
|
||||||
|
- Encourage the List → Map refactoring (but don't mandate it)
|
||||||
|
- Guide participants toward constructor validation
|
||||||
|
- Let them discover design patterns through pain points
|
||||||
|
- Celebrate when they refactor successfully!
|
||||||
|
|
||||||
|
The best learning happens when participants feel the design tension, then resolve it through refactoring.
|
||||||
62
shopping_cart/README.md
Normal file
62
shopping_cart/README.md
Normal file
|
|
@ -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!)
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Hint for Step 3 — Remove by name</summary>
|
||||||
|
|
||||||
|
When you need to remove by name, a `List<CartItem>` forces you to search by name.
|
||||||
|
A `Map<String, CartItem>` makes lookup O(1) and removal trivial.
|
||||||
|
Let the test drive you toward the right data structure.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Hint for Step 5 — Value Object</summary>
|
||||||
|
|
||||||
|
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.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Hint for the float precision bonus test</summary>
|
||||||
|
|
||||||
|
Use `closeTo(expected, delta)` instead of `equals()` for floating point comparisons.
|
||||||
|
Example: `expect(result, closeTo(0.30, 0.001))`
|
||||||
|
</details>
|
||||||
46
shopping_cart/lib/shopping_cart.dart
Normal file
46
shopping_cart/lib/shopping_cart.dart
Normal file
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
144
shopping_cart/lib/shopping_cart_solution.dart
Normal file
144
shopping_cart/lib/shopping_cart_solution.dart
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
// REFERENCE SOLUTION — For facilitator use only.
|
||||||
|
// Do not share with participants before the session.
|
||||||
|
//
|
||||||
|
// This solution shows the evolution of design through TDD:
|
||||||
|
// - Steps 1-2: Basic implementation with List
|
||||||
|
// - Step 3: Refactored to Map when removeItem() test drove the change
|
||||||
|
// - Step 5: Value Object pattern for CartItem validation
|
||||||
|
|
||||||
|
// STEP 5: Value Object pattern emerged from test "rejects item with zero price"
|
||||||
|
// See: test/shopping_cart_test.dart:139
|
||||||
|
//
|
||||||
|
// WHY VALIDATE IN CONSTRUCTOR?
|
||||||
|
// - CartItem can't exist in an invalid state
|
||||||
|
// - No need to check validity every time we use it
|
||||||
|
// - Validation happens once, at creation time
|
||||||
|
// - This is the Value Object pattern from Domain-Driven Design
|
||||||
|
class CartItem {
|
||||||
|
final String name;
|
||||||
|
final double price;
|
||||||
|
final int quantity;
|
||||||
|
|
||||||
|
CartItem({required this.name, required this.price, required this.quantity}) {
|
||||||
|
// STEP 5: From test "rejects item with zero/negative price"
|
||||||
|
// See: test/shopping_cart_test.dart:139-152
|
||||||
|
// Validation belongs HERE, not in ShoppingCart.addItem()
|
||||||
|
// If validation was in addItem(), what happens when CartItem is created elsewhere?
|
||||||
|
if (price <= 0) {
|
||||||
|
throw ArgumentError('Price must be greater than zero, got: $price');
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP 5: From test "rejects item with zero quantity"
|
||||||
|
// See: test/shopping_cart_test.dart:155-161
|
||||||
|
if (quantity <= 0) {
|
||||||
|
throw ArgumentError('Quantity must be greater than zero, got: $quantity');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShoppingCart {
|
||||||
|
// STEP 3 REFACTOR: Originally a List<CartItem>
|
||||||
|
// Changed to Map when removeItem(name) test made List lookup inefficient
|
||||||
|
//
|
||||||
|
// WHY MAP instead of LIST?
|
||||||
|
// - removeItem() needs to find item by name
|
||||||
|
// - With List: O(n) lookup with .removeWhere()
|
||||||
|
// - With Map: O(1) lookup by key
|
||||||
|
// - The test for "remove by name" drove this design decision
|
||||||
|
//
|
||||||
|
// From test "removing an item reduces the subtotal"
|
||||||
|
// See: test/shopping_cart_test.dart:71
|
||||||
|
final Map<String, CartItem> _items = {};
|
||||||
|
|
||||||
|
// STEP 2: From test "subtotal reflects a single item added"
|
||||||
|
// See: test/shopping_cart_test.dart:39
|
||||||
|
// Simple add - using name as key means duplicate names replace (design choice)
|
||||||
|
void addItem(CartItem item) {
|
||||||
|
_items[item.name] = item;
|
||||||
|
|
||||||
|
// DESIGN CHOICE: This replaces items with duplicate names
|
||||||
|
// Could alternatively accumulate: _items[item.name].quantity += item.quantity
|
||||||
|
// Both are valid! TDD forces us to decide and document via tests
|
||||||
|
// See bonus test: test/shopping_cart_test.dart:168
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP 3: From test "removing an item reduces the subtotal"
|
||||||
|
// See: test/shopping_cart_test.dart:71
|
||||||
|
// Map.remove() is simple and efficient - this test drove the Map refactoring
|
||||||
|
void removeItem(String name) {
|
||||||
|
_items.remove(name);
|
||||||
|
|
||||||
|
// Note: remove() on non-existent key does nothing (which is what we want)
|
||||||
|
// Test "removing non-existent item does nothing" confirmed this behavior
|
||||||
|
// See: test/shopping_cart_test.dart:87
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP 2: From test "subtotal reflects a single item added"
|
||||||
|
// See: test/shopping_cart_test.dart:39
|
||||||
|
// Classic fold operation: accumulate (price × quantity) for each item
|
||||||
|
double subtotal() {
|
||||||
|
return _items.values
|
||||||
|
.fold(0.0, (sum, item) => sum + (item.price * item.quantity));
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP 2: From test "item count reflects quantity of single item"
|
||||||
|
// See: test/shopping_cart_test.dart:45
|
||||||
|
// Similar to subtotal, but sum quantities instead of prices
|
||||||
|
int itemCount() {
|
||||||
|
return _items.values.fold(0, (sum, item) => sum + item.quantity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP 4: From test "10% discount reduces subtotal correctly"
|
||||||
|
// See: test/shopping_cart_test.dart:109
|
||||||
|
// Straightforward percentage calculation
|
||||||
|
// discountPercent is 0-100, so divide by 100 to get decimal
|
||||||
|
double totalAfterDiscount(double discountPercent) {
|
||||||
|
final discount = discountPercent / 100;
|
||||||
|
return subtotal() * (1 - discount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EVOLUTION NOTES:
|
||||||
|
//
|
||||||
|
// Initial Implementation (Steps 1-2):
|
||||||
|
// Started with List<CartItem> because that's the obvious choice:
|
||||||
|
//
|
||||||
|
// final List<CartItem> _items = [];
|
||||||
|
//
|
||||||
|
// void addItem(CartItem item) {
|
||||||
|
// _items.add(item);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// This worked for add, subtotal, and itemCount tests.
|
||||||
|
//
|
||||||
|
// REFACTORING TRIGGER (Step 3):
|
||||||
|
// The removeItem(String name) test exposed inefficiency:
|
||||||
|
//
|
||||||
|
// void removeItem(String name) {
|
||||||
|
// _items.removeWhere((item) => item.name == name); // O(n) search!
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// This works, but felt wrong. The test made us ask:
|
||||||
|
// "Why are we searching by name every time?"
|
||||||
|
//
|
||||||
|
// Refactoring to Map made the code simpler AND more efficient:
|
||||||
|
// _items.remove(name); // O(1) lookup!
|
||||||
|
//
|
||||||
|
// VALUE OBJECT INSIGHT (Step 5):
|
||||||
|
// Initially, participants might put validation in addItem():
|
||||||
|
//
|
||||||
|
// void addItem(CartItem item) {
|
||||||
|
// if (item.price <= 0) throw ArgumentError('Invalid price');
|
||||||
|
// _items[item.name] = item;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// But this has a problem: what if CartItem is created elsewhere?
|
||||||
|
// Moving validation to CartItem's constructor makes invalid state impossible.
|
||||||
|
//
|
||||||
|
// This is the Value Object pattern:
|
||||||
|
// - Immutable data
|
||||||
|
// - Validation at construction
|
||||||
|
// - Can't exist in invalid state
|
||||||
|
// - Can be passed around safely without rechecking validity
|
||||||
|
//
|
||||||
|
// All these design insights were DRIVEN BY TESTS, not planned upfront!
|
||||||
8
shopping_cart/pubspec.yaml
Normal file
8
shopping_cart/pubspec.yaml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
name: shopping_cart
|
||||||
|
description: TDD Workshop - Feature B
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ^3.0.0
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
test: ^1.25.0
|
||||||
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