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:
fiatcode 2026-03-10 15:37:58 +07:00
commit 3d94c96ed2
13 changed files with 1469 additions and 0 deletions

22
.gitignore vendored Normal file
View 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
View 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
View 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
View 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

@ -0,0 +1 @@
Subproject commit 51ab5159f4c8407cd47289c8e06b401b9774b821

View 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 (AZ) | `Must contain at least one uppercase letter` |
| 3 | At least one digit (09) | `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?

View 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();
}
}

View file

@ -0,0 +1,8 @@
name: password_validator
description: TDD Workshop - Feature A
environment:
sdk: ^3.0.0
dev_dependencies:
test: ^1.25.0

View 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 AZ 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 09 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
View 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>

View 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();
}
}

View file

@ -0,0 +1,8 @@
name: shopping_cart
description: TDD Workshop - Feature B
environment:
sdk: ^3.0.0
dev_dependencies:
test: ^1.25.0

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