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:
commit
3d94c96ed2
13 changed files with 1469 additions and 0 deletions
43
password_validator/README.md
Normal file
43
password_validator/README.md
Normal 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 (A–Z) | `Must contain at least one uppercase letter` |
|
||||
| 3 | At least one digit (0–9) | `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?
|
||||
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();
|
||||
}
|
||||
}
|
||||
8
password_validator/pubspec.yaml
Normal file
8
password_validator/pubspec.yaml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
name: password_validator
|
||||
description: TDD Workshop - Feature A
|
||||
|
||||
environment:
|
||||
sdk: ^3.0.0
|
||||
|
||||
dev_dependencies:
|
||||
test: ^1.25.0
|
||||
157
password_validator/test/password_validator_test.dart
Normal file
157
password_validator/test/password_validator_test.dart
Normal 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 A–Z 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 0–9 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue