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

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