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:
fiatcode 2026-03-10 15:32:21 +07:00
commit c3355063f2
26 changed files with 4725 additions and 0 deletions

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,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!