Initial commit: Complete TDD workshop materials
- Workshop documentation (WORKSHOP_PLAN, FACILITATOR_GUIDE, etc.) - FizzBuzz kata with demo script (git history to be recreated) - Password Validator kata with demo script and solution - Shopping Cart kata with demo script and solution - Setup guide and TDD reference card for participants
This commit is contained in:
commit
c3355063f2
26 changed files with 4725 additions and 0 deletions
372
password_validator/DEMO_SCRIPT.md
Normal file
372
password_validator/DEMO_SCRIPT.md
Normal file
|
|
@ -0,0 +1,372 @@
|
|||
# Password Validator — Demo Script for Facilitators
|
||||
|
||||
**Purpose:** This script helps you demonstrate the Password Validator kata if participants get stuck, or for debriefing discussions. Unlike the FizzBuzz live demo, this is NOT meant to be performed in real-time. Use it as a reference guide.
|
||||
|
||||
**Target time:** 45 minutes (participant hands-on)
|
||||
**Your role:** Circulate, answer questions, refer to this script when helping stuck participants
|
||||
|
||||
---
|
||||
|
||||
## Overview of the Kata
|
||||
|
||||
**Validation rules to implement (in order):**
|
||||
|
||||
1. At least 8 characters long
|
||||
2. At least one uppercase letter (A–Z)
|
||||
3. At least one digit (0–9)
|
||||
4. At least one special character (!@#$%^&*)
|
||||
5. No spaces
|
||||
6. **Refactor:** All errors reported at once
|
||||
|
||||
**Key learning goals:**
|
||||
- Practice RED-GREEN-REFACTOR rhythm
|
||||
- Experience code evolution through tests
|
||||
- Discover the Open-Closed Principle naturally (Step 6)
|
||||
|
||||
---
|
||||
|
||||
## Suggested Test Order & Implementation
|
||||
|
||||
### STEP 1: Minimum Length (Start Simple)
|
||||
|
||||
**First test to uncomment (line 23):**
|
||||
```dart
|
||||
test('rejects a password shorter than 8 characters', () {
|
||||
final result = validator.validate('Ab1!xyz');
|
||||
expect(result.isValid, isFalse);
|
||||
expect(result.errors, contains('Must be at least 8 characters long'));
|
||||
});
|
||||
```
|
||||
|
||||
**Expected failure:**
|
||||
- "The method 'validate' isn't defined for the class 'PasswordValidator'"
|
||||
|
||||
**Minimum code to make it GREEN:**
|
||||
```dart
|
||||
class ValidationResult {
|
||||
final bool isValid;
|
||||
final List<String> errors;
|
||||
|
||||
const ValidationResult({required this.isValid, required this.errors});
|
||||
}
|
||||
|
||||
class PasswordValidator {
|
||||
ValidationResult validate(String password) {
|
||||
if (password.length < 8) {
|
||||
return ValidationResult(
|
||||
isValid: false,
|
||||
errors: ['Must be at least 8 characters long']
|
||||
);
|
||||
}
|
||||
return ValidationResult(isValid: true, errors: []);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Next tests (lines 30, 36):**
|
||||
- "accepts exactly 8 characters" → already passes!
|
||||
- "accepts longer than 8 characters" → already passes!
|
||||
|
||||
**Teaching moment:** "Notice how the general solution covered the edge cases automatically."
|
||||
|
||||
---
|
||||
|
||||
### STEP 2: Uppercase Letter Required
|
||||
|
||||
**Test to uncomment (line 48):**
|
||||
```dart
|
||||
test('rejects a password with no uppercase letters', () {
|
||||
final result = validator.validate('ab1!xyzw');
|
||||
expect(result.isValid, isFalse);
|
||||
expect(result.errors, contains('Must contain at least one uppercase letter'));
|
||||
});
|
||||
```
|
||||
|
||||
**Expected failure:**
|
||||
- Test fails because 'ab1!xyzw' is 8 chars, so it passes validation
|
||||
|
||||
**Minimum code to make it GREEN:**
|
||||
```dart
|
||||
ValidationResult validate(String password) {
|
||||
final errors = <String>[];
|
||||
|
||||
if (password.length < 8) {
|
||||
errors.add('Must be at least 8 characters long');
|
||||
}
|
||||
|
||||
if (!password.contains(RegExp(r'[A-Z]'))) {
|
||||
errors.add('Must contain at least one uppercase letter');
|
||||
}
|
||||
|
||||
return ValidationResult(
|
||||
isValid: errors.isEmpty,
|
||||
errors: errors,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Teaching moment:** "We switched from single error to a list. This is the design emerging from the tests."
|
||||
|
||||
---
|
||||
|
||||
### STEP 3: Digit Required
|
||||
|
||||
**Test to uncomment (line 67):**
|
||||
```dart
|
||||
test('rejects a password with no digits', () {
|
||||
final result = validator.validate('Ab!!xyzw');
|
||||
expect(result.isValid, isFalse);
|
||||
expect(result.errors, contains('Must contain at least one digit'));
|
||||
});
|
||||
```
|
||||
|
||||
**Code to add:**
|
||||
```dart
|
||||
if (!password.contains(RegExp(r'[0-9]'))) {
|
||||
errors.add('Must contain at least one digit');
|
||||
}
|
||||
```
|
||||
|
||||
**Teaching moment:** "Notice the pattern? Each rule is just another if-block. We'll refactor this later."
|
||||
|
||||
---
|
||||
|
||||
### STEP 4: Special Character Required
|
||||
|
||||
**Test to uncomment (line 86):**
|
||||
```dart
|
||||
test('rejects a password with no special characters', () {
|
||||
final result = validator.validate('Ab1xxxyw');
|
||||
expect(result.isValid, isFalse);
|
||||
expect(result.errors, contains('Must contain at least one special character (!@#\$%^&*)'));
|
||||
});
|
||||
```
|
||||
|
||||
**Code to add:**
|
||||
```dart
|
||||
if (!password.contains(RegExp(r'[!@#$%^&*]'))) {
|
||||
errors.add(r'Must contain at least one special character (!@#$%^&*)');
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### STEP 5: No Spaces Allowed
|
||||
|
||||
**Test to uncomment (line 104):**
|
||||
```dart
|
||||
test('rejects a password that contains a space', () {
|
||||
final result = validator.validate('Ab1! xyz');
|
||||
expect(result.isValid, isFalse);
|
||||
expect(result.errors, contains('Must not contain spaces'));
|
||||
});
|
||||
```
|
||||
|
||||
**Code to add:**
|
||||
```dart
|
||||
if (password.contains(' ')) {
|
||||
errors.add('Must not contain spaces');
|
||||
}
|
||||
```
|
||||
|
||||
**At this point, all tests pass! ✅ But the code has duplication...**
|
||||
|
||||
---
|
||||
|
||||
### STEP 6: Refactor to Rule Functions (The Insight)
|
||||
|
||||
**Test to uncomment (line 124):**
|
||||
```dart
|
||||
test('reports all failures for a completely invalid password', () {
|
||||
final result = validator.validate('bad');
|
||||
expect(result.isValid, isFalse);
|
||||
expect(result.errors.length, equals(4)); // length, uppercase, digit, special char
|
||||
});
|
||||
```
|
||||
|
||||
**This test already passes!** But now we refactor for maintainability.
|
||||
|
||||
**Before refactoring, the validate() method looks like:**
|
||||
```dart
|
||||
ValidationResult validate(String password) {
|
||||
final errors = <String>[];
|
||||
if (password.length < 8) errors.add('Must be at least 8 characters long');
|
||||
if (!password.contains(RegExp(r'[A-Z]'))) errors.add('Must contain at least one uppercase letter');
|
||||
if (!password.contains(RegExp(r'[0-9]'))) errors.add('Must contain at least one digit');
|
||||
if (!password.contains(RegExp(r'[!@#$%^&*]'))) errors.add(r'Must contain at least one special character (!@#$%^&*)');
|
||||
if (password.contains(' ')) errors.add('Must not contain spaces');
|
||||
return ValidationResult(isValid: errors.isEmpty, errors: errors);
|
||||
}
|
||||
```
|
||||
|
||||
**Problem:** Adding a 6th rule means modifying this method. What if we had 20 rules?
|
||||
|
||||
**Refactoring insight:** Extract each rule into its own function.
|
||||
|
||||
**After refactoring:**
|
||||
```dart
|
||||
typedef PasswordRule = String? Function(String password);
|
||||
|
||||
class PasswordValidator {
|
||||
static final List<PasswordRule> _rules = [
|
||||
_minimumLength,
|
||||
_requiresUppercase,
|
||||
_requiresDigit,
|
||||
_requiresSpecialCharacter,
|
||||
_noSpaces,
|
||||
];
|
||||
|
||||
ValidationResult validate(String password) {
|
||||
final errors = _rules
|
||||
.map((rule) => rule(password))
|
||||
.whereType<String>()
|
||||
.toList();
|
||||
|
||||
return ValidationResult(
|
||||
isValid: errors.isEmpty,
|
||||
errors: errors,
|
||||
);
|
||||
}
|
||||
|
||||
static String? _minimumLength(String password) {
|
||||
if (password.length < 8) {
|
||||
return 'Must be at least 8 characters long';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? _requiresUppercase(String password) {
|
||||
if (!password.contains(RegExp(r'[A-Z]'))) {
|
||||
return 'Must contain at least one uppercase letter';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? _requiresDigit(String password) {
|
||||
if (!password.contains(RegExp(r'[0-9]'))) {
|
||||
return 'Must contain at least one digit';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? _requiresSpecialCharacter(String password) {
|
||||
if (!password.contains(RegExp(r'[!@#$%^&*]'))) {
|
||||
return r'Must contain at least one special character (!@#$%^&*)';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? _noSpaces(String password) {
|
||||
if (password.contains(' ')) {
|
||||
return 'Must not contain spaces';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Run tests again → Still GREEN! ✅**
|
||||
|
||||
**Teaching moment:** "This is the Open-Closed Principle in action:
|
||||
- **Open for extension:** Add a new rule? Just add it to the `_rules` list.
|
||||
- **Closed for modification:** The `validate()` method never needs to change.
|
||||
|
||||
This refactoring was safe because the tests stayed green throughout!"
|
||||
|
||||
---
|
||||
|
||||
## Common Participant Questions
|
||||
|
||||
### "Why return null instead of false?"
|
||||
|
||||
**Answer:** Returning `null` means "no error" while returning a String means "here's the error message." This pattern is common in Dart (nullable types) and makes the code more expressive than true/false.
|
||||
|
||||
### "Can I combine all the RegExp patterns into one big pattern?"
|
||||
|
||||
**Answer:** You could, but then you lose granular error messages. Try it and see what happens to the tests! (They'll fail because you can't report specific errors.)
|
||||
|
||||
### "Why use static methods for rules?"
|
||||
|
||||
**Answer:** Rules don't need instance state—they just take a password and return an error. Static methods make this clear. You could also use top-level functions.
|
||||
|
||||
### "Should I test the individual rule functions?"
|
||||
|
||||
**Answer:** Not necessary in this kata—they're private implementation details. The public `validate()` method is fully tested through its behavior. However, in real production code, you might extract rules to separate files and test them individually.
|
||||
|
||||
---
|
||||
|
||||
## Debrief Discussion Points (5-10 min after kata)
|
||||
|
||||
Ask participants:
|
||||
|
||||
1. **"At what point did you notice the duplication?"**
|
||||
- Most notice it around Step 3-4
|
||||
- Some push through to Step 5 before refactoring
|
||||
|
||||
2. **"What made you decide to refactor?"**
|
||||
- Listen for: "too much repetition," "hard to add new rules," "messy code"
|
||||
- Reinforce: Tests gave you confidence to refactor safely
|
||||
|
||||
3. **"How did the tests help during refactoring?"**
|
||||
- They stayed green → proving the behavior didn't change
|
||||
- Could experiment freely knowing tests would catch mistakes
|
||||
|
||||
4. **"What's the benefit of the rule functions approach?"**
|
||||
- Easier to add new rules (just add to list)
|
||||
- Each rule is isolated and easier to understand
|
||||
- Could even load rules from configuration!
|
||||
|
||||
5. **"Did anyone try a different approach?"**
|
||||
- Some might use class-based rules, strategy pattern, etc.
|
||||
- All valid! TDD doesn't prescribe the design, just guides it
|
||||
|
||||
---
|
||||
|
||||
## Timing Guidance
|
||||
|
||||
**For 45-minute practice session:**
|
||||
- 0-5 min: Participants read README, set up files
|
||||
- 5-25 min: Implement Steps 1-5 (about 4 min per rule)
|
||||
- 25-35 min: Refactor Step 6
|
||||
- 35-40 min: Bonus edge cases (if time)
|
||||
- 40-45 min: Quick debrief
|
||||
|
||||
**If participants finish early:**
|
||||
- Direct them to bonus edge cases in the tests
|
||||
- Challenge: "Can you add a 6th rule without modifying validate()?"
|
||||
- Suggest: "Try extracting rules to a separate file"
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Participant stuck on first test
|
||||
- Check: Did they create `ValidationResult` class?
|
||||
- Check: Did they create `validate()` method?
|
||||
- Hint: "Start with the absolute minimum to make it compile and fail correctly"
|
||||
|
||||
### Tests not running
|
||||
- Check: Did they run `dart pub get`?
|
||||
- Check: Are they in the `password_validator` directory?
|
||||
- Check: Did they uncomment the test code?
|
||||
|
||||
### Participant trying to write all code at once
|
||||
- Remind: "Just make *this one test* pass. Nothing more."
|
||||
- Ask: "What's the simplest code that could work?"
|
||||
|
||||
### Participant stuck on refactoring
|
||||
- Ask: "What pattern do you see repeated?"
|
||||
- Hint: "What if each rule was a function?"
|
||||
- Show: The typedef example from the solution
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
This kata demonstrates:
|
||||
- ✅ The RED-GREEN-REFACTOR rhythm in practice
|
||||
- ✅ How tests enable safe refactoring
|
||||
- ✅ The Open-Closed Principle emerging naturally from TDD
|
||||
- ✅ Design evolution driven by tests, not upfront planning
|
||||
|
||||
**Key facilitator role:** Encourage participants to feel the pain of duplication *before* refactoring. The refactoring is more meaningful when they discover it themselves.
|
||||
Loading…
Add table
Add a link
Reference in a new issue