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