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
25
password_validator/lib/password_validator.dart
Normal file
25
password_validator/lib/password_validator.dart
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// TODO: Implement PasswordValidator using TDD.
|
||||
//
|
||||
// Rules (implement one at a time, test first!):
|
||||
// 1. Must be at least 8 characters long
|
||||
// 2. Must contain at least one uppercase letter
|
||||
// 3. Must contain at least one digit
|
||||
// 4. Must contain at least one special character (!@#$%^&*)
|
||||
// 5. Must not contain spaces
|
||||
//
|
||||
// ValidationResult holds a pass/fail flag AND a list of error messages.
|
||||
// Never return a boolean — the caller deserves to know *why* it failed.
|
||||
|
||||
class ValidationResult {
|
||||
final bool isValid;
|
||||
final List<String> errors;
|
||||
|
||||
const ValidationResult({required this.isValid, required this.errors});
|
||||
}
|
||||
|
||||
class PasswordValidator {
|
||||
ValidationResult validate(String password) {
|
||||
// TODO: implement
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
127
password_validator/lib/password_validator_solution.dart
Normal file
127
password_validator/lib/password_validator_solution.dart
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
// REFERENCE SOLUTION — For facilitator use only.
|
||||
// Do not share with participants before the session.
|
||||
//
|
||||
// This solution shows the evolution of the code through TDD:
|
||||
// - Steps 1-5: Individual rules implemented as separate if-blocks
|
||||
// - Step 6: Refactored to use rule functions (Open-Closed Principle)
|
||||
|
||||
class ValidationResult {
|
||||
final bool isValid;
|
||||
final List<String> errors;
|
||||
|
||||
const ValidationResult({required this.isValid, required this.errors});
|
||||
}
|
||||
|
||||
// STEP 6 REFACTOR: This typedef emerged after implementing all 5 rules
|
||||
// It represents the pattern: "A rule takes a password and returns an error message or null"
|
||||
// Defining this makes the refactoring much cleaner
|
||||
typedef PasswordRule = String? Function(String password);
|
||||
|
||||
class PasswordValidator {
|
||||
// STEP 6 REFACTOR: Originally had 5 separate if-blocks in validate()
|
||||
// Tests revealed the duplication - each rule followed the same pattern
|
||||
// Extracted each rule into its own function and collected them in a list
|
||||
// This is the Open-Closed Principle: open for extension (add new rule to list),
|
||||
// closed for modification (validate() method never changes)
|
||||
static final List<PasswordRule> _rules = [
|
||||
_minimumLength, // STEP 1
|
||||
_requiresUppercase, // STEP 2
|
||||
_requiresDigit, // STEP 3
|
||||
_requiresSpecialCharacter, // STEP 4
|
||||
_noSpaces, // STEP 5
|
||||
];
|
||||
|
||||
ValidationResult validate(String password) {
|
||||
// STEP 6 REFACTOR: This elegant implementation emerged from refactoring
|
||||
// It runs all rules, filters out null (passing rules), and collects errors
|
||||
// Before refactoring, this was 20+ lines of nested if-statements
|
||||
final errors = _rules
|
||||
.map((rule) => rule(password))
|
||||
.whereType<String>() // Filters out nulls (rules that passed)
|
||||
.toList();
|
||||
|
||||
return ValidationResult(
|
||||
isValid: errors.isEmpty,
|
||||
errors: errors,
|
||||
);
|
||||
}
|
||||
|
||||
// STEP 1: From test "rejects password shorter than 8 characters"
|
||||
// See: test/password_validator_test.dart:23
|
||||
// This was the first rule implemented - started simple
|
||||
static String? _minimumLength(String password) {
|
||||
if (password.length < 8) {
|
||||
return 'Must be at least 8 characters long';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// STEP 2: From test "rejects password with no uppercase letters"
|
||||
// See: test/password_validator_test.dart:48
|
||||
// Used RegExp to check for at least one A-Z character
|
||||
static String? _requiresUppercase(String password) {
|
||||
if (!password.contains(RegExp(r'[A-Z]'))) {
|
||||
return 'Must contain at least one uppercase letter';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// STEP 3: From test "rejects password with no digits"
|
||||
// See: test/password_validator_test.dart:67
|
||||
// Pattern similar to uppercase - started to see duplication here
|
||||
static String? _requiresDigit(String password) {
|
||||
if (!password.contains(RegExp(r'[0-9]'))) {
|
||||
return 'Must contain at least one digit';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// STEP 4: From test "rejects password with no special characters"
|
||||
// See: test/password_validator_test.dart:86
|
||||
// RegExp gets more specific - only certain special chars allowed
|
||||
static String? _requiresSpecialCharacter(String password) {
|
||||
if (!password.contains(RegExp(r'[!@#$%^&*]'))) {
|
||||
return r'Must contain at least one special character (!@#$%^&*)';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// STEP 5: From test "rejects password that contains a space"
|
||||
// See: test/password_validator_test.dart:104
|
||||
// Simplest check - just look for space character
|
||||
static String? _noSpaces(String password) {
|
||||
if (password.contains(' ')) {
|
||||
return 'Must not contain spaces';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// EVOLUTION NOTES:
|
||||
//
|
||||
// Initial Implementation (Steps 1-5):
|
||||
// The validate() method looked like this:
|
||||
//
|
||||
// ValidationResult validate(String password) {
|
||||
// final errors = <String>[];
|
||||
// if (password.length < 8) errors.add('Must be at least 8 characters long');
|
||||
// if (!password.contains(RegExp(r'[A-Z]'))) errors.add('Must contain at least one uppercase letter');
|
||||
// if (!password.contains(RegExp(r'[0-9]'))) errors.add('Must contain at least one digit');
|
||||
// if (!password.contains(RegExp(r'[!@#$%^&*]'))) errors.add(r'Must contain at least one special character (!@#$%^&*)');
|
||||
// if (password.contains(' ')) errors.add('Must not contain spaces');
|
||||
// return ValidationResult(isValid: errors.isEmpty, errors: errors);
|
||||
// }
|
||||
//
|
||||
// This worked! All tests passed. But...
|
||||
//
|
||||
// REFACTORING INSIGHT (Step 6):
|
||||
// - Each rule follows the same pattern: check condition, add error if fails
|
||||
// - Adding a 6th rule means modifying validate() method (violates Open-Closed)
|
||||
// - What if we had 20 rules? 50 rules? This becomes unreadable
|
||||
//
|
||||
// Solution: Extract each rule into its own function
|
||||
// - Each function is independent and testable
|
||||
// - Adding new rule = adding one line to _rules list
|
||||
// - validate() method never needs to change
|
||||
//
|
||||
// This refactoring was driven by the tests - they stayed GREEN throughout!
|
||||
Loading…
Add table
Add a link
Reference in a new issue