Initial commit: TDD Workshop exercises for participants
- Password Validator kata with starter code and tests - Shopping Cart kata with starter code and tests - FizzBuzz reference code (from live demo) - Setup guide and TDD reference card - No solutions included (participants implement themselves)
This commit is contained in:
commit
3d94c96ed2
13 changed files with 1469 additions and 0 deletions
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
# Dart build artifacts
|
||||||
|
.dart_tool/
|
||||||
|
.packages
|
||||||
|
pubspec.lock
|
||||||
|
build/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
|
|
||||||
|
# FizzBuzz nested git repository (keep the .git for demo purposes)
|
||||||
|
# The fizzbuzz/.git directory contains TDD progression commits
|
||||||
|
# It's intentionally kept separate from the main workshop repo
|
||||||
200
README.md
Normal file
200
README.md
Normal file
|
|
@ -0,0 +1,200 @@
|
||||||
|
# TDD Workshop - Exercises
|
||||||
|
|
||||||
|
**A hands-on, 2-hour workshop for learning Test-Driven Development through deliberate practice.**
|
||||||
|
|
||||||
|
This repository contains the exercise materials for the TDD workshop. Follow the setup guide to prepare before the workshop.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Before the Workshop
|
||||||
|
|
||||||
|
### 1. Complete Setup
|
||||||
|
|
||||||
|
Follow the **[SETUP_GUIDE.md](SETUP_GUIDE.md)** to:
|
||||||
|
- Install Dart SDK
|
||||||
|
- Verify your development environment
|
||||||
|
- Run a test to confirm everything works
|
||||||
|
|
||||||
|
### 2. Verify Installation
|
||||||
|
|
||||||
|
Test that everything is working:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd password_validator
|
||||||
|
dart pub get
|
||||||
|
dart test # Should see "All tests skipped" or similar
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Bookmark Reference
|
||||||
|
|
||||||
|
Keep **[TDD_REFERENCE_CARD.md](TDD_REFERENCE_CARD.md)** handy during the workshop for quick reference on the RED-GREEN-REFACTOR cycle.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Repository Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
tdd-workshop-exercises/
|
||||||
|
├── README.md (you are here)
|
||||||
|
├── SETUP_GUIDE.md # Setup instructions
|
||||||
|
├── TDD_REFERENCE_CARD.md # Quick reference for TDD
|
||||||
|
│
|
||||||
|
├── fizzbuzz/ # Live demo kata (reference)
|
||||||
|
│ ├── README.md
|
||||||
|
│ ├── lib/fizzbuzz.dart
|
||||||
|
│ └── test/fizzbuzz_test.dart
|
||||||
|
│
|
||||||
|
├── password_validator/ # Hands-on exercise (Option A)
|
||||||
|
│ ├── README.md # Exercise instructions
|
||||||
|
│ ├── lib/password_validator.dart # TODO: Implement here
|
||||||
|
│ └── test/password_validator_test.dart # Uncomment tests step-by-step
|
||||||
|
│
|
||||||
|
└── shopping_cart/ # Hands-on exercise (Option B)
|
||||||
|
├── README.md # Exercise instructions
|
||||||
|
├── lib/shopping_cart.dart # TODO: Implement here
|
||||||
|
└── test/shopping_cart_test.dart # Uncomment tests step-by-step
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## During the Workshop
|
||||||
|
|
||||||
|
### Part 1: Live Demo (15 min)
|
||||||
|
|
||||||
|
Watch the facilitator demonstrate the RED-GREEN-REFACTOR cycle with **FizzBuzz**.
|
||||||
|
|
||||||
|
The `fizzbuzz/` directory contains reference code you can review later.
|
||||||
|
|
||||||
|
### Part 2: Choose Your Kata (45 min)
|
||||||
|
|
||||||
|
Pick **one** kata to practice:
|
||||||
|
|
||||||
|
#### Option A: Password Validator
|
||||||
|
- Build a password validator with 5 rules
|
||||||
|
- Practice rule-based validation
|
||||||
|
- Discover the Open-Closed Principle through refactoring
|
||||||
|
|
||||||
|
**Start here:** `password_validator/README.md`
|
||||||
|
|
||||||
|
#### Option B: Shopping Cart
|
||||||
|
- Build a shopping cart with add, remove, discount
|
||||||
|
- Practice stateful domain modeling
|
||||||
|
- Discover when to use Map vs List
|
||||||
|
- Learn the Value Object pattern
|
||||||
|
|
||||||
|
**Start here:** `shopping_cart/README.md`
|
||||||
|
|
||||||
|
### Part 3: Debrief & Reflection (10 min)
|
||||||
|
|
||||||
|
Share learnings and discuss design insights with the group.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The TDD Workflow
|
||||||
|
|
||||||
|
For each kata, follow this rhythm:
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Uncomment the next test
|
||||||
|
2. Run tests → watch it FAIL (RED)
|
||||||
|
3. Write minimal code to make it pass (GREEN)
|
||||||
|
4. Refactor if needed → tests stay GREEN
|
||||||
|
5. Commit your work
|
||||||
|
6. Repeat with the next test
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key principles:**
|
||||||
|
- Write the test BEFORE the implementation
|
||||||
|
- Take small steps (one test at a time)
|
||||||
|
- Make it work first, make it clean second
|
||||||
|
- Let the tests drive your design
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Running the Katas
|
||||||
|
|
||||||
|
### Password Validator
|
||||||
|
```bash
|
||||||
|
cd password_validator
|
||||||
|
dart pub get
|
||||||
|
dart test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Shopping Cart
|
||||||
|
```bash
|
||||||
|
cd shopping_cart
|
||||||
|
dart pub get
|
||||||
|
dart test
|
||||||
|
```
|
||||||
|
|
||||||
|
### FizzBuzz (reference)
|
||||||
|
```bash
|
||||||
|
cd fizzbuzz
|
||||||
|
dart pub get
|
||||||
|
dart test
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
Having issues? See **[SETUP_GUIDE.md](SETUP_GUIDE.md)** for:
|
||||||
|
- Dart installation problems
|
||||||
|
- `dart pub get` failures
|
||||||
|
- Test execution errors
|
||||||
|
- IDE configuration
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## After the Workshop
|
||||||
|
|
||||||
|
### Continue Practicing
|
||||||
|
|
||||||
|
Explore more katas at:
|
||||||
|
- **TDD Katas Collection:** https://github.com/dhemasnurjaya/tdd-katas
|
||||||
|
- **Kata-Log:** https://kata-log.rocks
|
||||||
|
- **Exercism:** https://exercism.org
|
||||||
|
- **Coding Dojo:** https://codingdojo.org
|
||||||
|
|
||||||
|
### Recommended Reading
|
||||||
|
|
||||||
|
- *Test-Driven Development* by Kent Beck
|
||||||
|
- *Clean Code* by Robert C. Martin
|
||||||
|
- *Growing Object-Oriented Software, Guided by Tests* by Freeman & Pryce
|
||||||
|
|
||||||
|
### Keep Practicing!
|
||||||
|
|
||||||
|
The **RED-GREEN-REFACTOR** cycle becomes second nature with practice:
|
||||||
|
1. Start with small, obvious katas
|
||||||
|
2. Practice the rhythm until it's automatic
|
||||||
|
3. Gradually tackle more complex problems
|
||||||
|
4. Focus on letting tests drive design decisions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Questions?
|
||||||
|
|
||||||
|
During the workshop:
|
||||||
|
- Raise your hand or ask your facilitator
|
||||||
|
- Don't hesitate to ask for help if you're stuck!
|
||||||
|
|
||||||
|
Before the workshop:
|
||||||
|
- Contact your workshop facilitator
|
||||||
|
- Verify your setup using SETUP_GUIDE.md
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Philosophy
|
||||||
|
|
||||||
|
> "Clean code that works." — Ron Jeffries
|
||||||
|
|
||||||
|
TDD is a **discipline**, not a burden:
|
||||||
|
- Tests first (even for "obvious" code)
|
||||||
|
- Small steps (one test at a time)
|
||||||
|
- Refactor fearlessly (tests protect you)
|
||||||
|
|
||||||
|
**The goal:** Build muscle memory for the RED-GREEN-REFACTOR cycle.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Ready to practice? See [SETUP_GUIDE.md](SETUP_GUIDE.md) to get started!**
|
||||||
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.**
|
||||||
1
fizzbuzz
Submodule
1
fizzbuzz
Submodule
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 51ab5159f4c8407cd47289c8e06b401b9774b821
|
||||||
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
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