10 KiB
TDD Katas Collection
A collection of Test-Driven Development (TDD) exercises implementing classic programming katas, following Uncle Bob's Clean Code principles and Domain-Driven Design tactical patterns.
Purpose
These katas demonstrate:
- Test-Driven Development: Production code written only to pass failing tests
- Clean Code Practices: Intent-revealing names, single responsibility, domain-focused abstractions
- Domain-Driven Design: Value Objects enforce domain constraints at the type level
- The Craftsman's Way: Quality is not a trade-off for speed; it is the only way to go fast
Completed Katas
1. Roman Numerals ✅
Implementation: lib/roman_numerals.dart
Tests: test/roman_numerals_test.dart
Converts integers (1-3999) to Roman numeral notation using a table-driven greedy algorithm.
Domain Context
Roman numerals represent numbers using seven basic symbols with specific combination rules:
Symbols:
I= 1,V= 5,X= 10,L= 50,C= 100,D= 500,M= 1000
Domain Rules:
- Additive Notation: Symbols placed in descending order are summed (e.g.,
VI= 6) - Subtractive Notation: Smaller symbol before larger subtracts (e.g.,
IV= 4) - Repetition Limit: Symbols repeat maximum three times (e.g.,
III= 3, notIIII) - Valid Range: Classical Roman numerals represent 1-3999
Subtractive Pairs (Domain Constraint): Only specific pairs use subtractive notation:
IV(4),IX(9)XL(40),XC(90)CD(400),CM(900)
Key Design Decisions
Value Object Pattern: RomanNumeralInput enforces domain invariants (1-3999 range). Invalid inputs are impossible to construct.
Table-Driven Algorithm: The conversionRules table is the domain model—it directly represents Roman numeral encoding rules.
Greedy Decomposition: The algorithm mirrors how Romans actually encoded numbers: repeatedly subtract the largest applicable value.
Usage
import 'package:tdd_katas/roman_numerals.dart';
integerToRoman(1994); // Returns: 'MCMXCIV'
integerToRoman(0); // Throws: ArgumentError
2. Bowling Game ✅
Implementation: lib/bowling_game.dart
Tests: test/bowling_game_test.dart
Calculates scores for a bowling game following official scoring rules with look-ahead bonus logic for strikes and spares.
Domain Context
A bowling game consists of 10 frames where players roll a ball to knock down pins:
Scoring Rules:
- Normal Frame: Sum of pins knocked down (e.g., 3 + 4 = 7)
- Spare (/): All 10 pins in 2 rolls → Score = 10 + next 1 roll
- Strike (X): All 10 pins in 1 roll → Score = 10 + next 2 rolls
- 10th Frame: Bonus rolls awarded if spare or strike achieved
Key Design Decisions
State Management: Stores all rolls in a list and calculates score by iterating through frames, not individual rolls.
Look-Ahead Logic: Spares and strikes require examining future rolls for bonus calculation—the algorithm walks forward strategically.
Frame Advancement: Strikes consume 1 roll, spares/normal frames consume 2 rolls—the algorithm tracks position correctly.
Usage
import 'package:tdd_katas/bowling_game.dart';
final game = BowlingGame();
game.roll(10); // Strike!
game.roll(3);
game.roll(4);
// ... continue rolling
print(game.score()); // Calculates total with bonuses
The "Aha!" Moment
Tests for simple cases (gutter game, one spare, one strike) drove an algorithm that automatically handles complex scenarios like perfect games (300 points) without explicit implementation. This is TDD's magic—correct abstractions emerge naturally.
3. [Third Kata Name] 🚧
Status: Not yet started
Implementation: lib/[third_kata].dart
Tests: test/[third_kata]_test.dart
Coming soon...
Running Tests
# Run all tests
dart test
# Run specific test file
dart test test/roman_numerals_test.dart
dart test test/bowling_game_test.dart
# Run with coverage
dart test --coverage
Development Philosophy
The Craftsman's Standard
"Clean code that works." — Ron Jeffries
Every kata in this collection follows:
- Uncle Bob's Clean Code: Intent-revealing names, functions do one thing, no comments needed
- Kent Beck's TDD: Red-Green-Refactor discipline, tests first
- Eric Evans' DDD: Domain concepts drive the model, tactical patterns enforce boundaries
- The Boy Scout Rule: Every commit leaves the code cleaner than before
TDD Discipline Applied
- Red: Write a failing test
- Green: Write the simplest code to pass
- Refactor: Clean up duplication, improve names
- Repeat: Let the design emerge from tests
Roman Numerals: Detailed Journey
Test Strategy
Tests are organized by domain concepts, not technical structure:
Basic Symbols: Tests for the seven fundamental symbols (I, V, X, L, C, D, M)
Subtractive Notation: Tests for all six subtractive pairs, verifying the domain rule
Additive Combinations: Tests for repeated symbols and multi-symbol sequences
Complex Edge Cases: Stress tests combining multiple rules:
1994 → MCMXCIV(year notation)3999 → MMMCMXCIX(maximum valid value)444 → CDXLIV(all subtractive positions)
Constraint Validation: Boundary tests for the valid range (1-3999)
Development Timeline
- Red: Tests for 1-5 (basic additive, first subtractive case)
- Green: Minimal implementation with conditionals
- Refactor: Extract symbol mapping, clarify intent
- Red: Tests for 6-10 (reveals pattern)
- Green: Extend conditionals
- Refactor: Recognize duplication → Table-driven approach emerges
- Red: Tests for 40-1000 (remaining symbols)
- Green: Extend conversion table (algorithm unchanged)
- Red: Edge cases and constraint tests
- Green: Add
RomanNumeralInputValue Object - Refactor: Extract validation, organize tests by domain concept
Key Insights
The Algorithm Never Changed: After the table-driven refactoring, adding 40-1000 required zero logic modifications. This validates the abstraction.
Type System as Domain Enforcer: RomanNumeralInput makes invalid states unrepresentable. You cannot construct a Roman numeral for 0 or 4000—the compiler prevents it.
Tests as Living Documentation: Test names use ubiquitous language from the Roman numeral domain. A domain expert could read the test file and recognize the rules they explained.
Bowling Game: Detailed Journey
Test Strategy
Tests are organized by scoring complexity, mirroring how the domain rules build on each other:
Basic Scoring:
- Gutter game (all zeros)
- All ones (simple addition)
Spare Bonus (next 1 roll):
- One spare in first frame
- All spares (150 points)
Strike Bonus (next 2 rolls):
- One strike in first frame
- Perfect game (300 points)
Complex Scenarios:
- Combinations of strikes, spares, and normal frames
Development Timeline
- Red: Gutter game test
- Green: Return 0 (simplest implementation)
- Red: All ones test
- Green: Store rolls, sum them in
score() - Refactor: Extract
rollMany()helper, addsetUp() - Red: One spare test
- Green: Detect spare, add look-ahead bonus (+1 roll)
- Refactor: Extract
_isSpare()helper - Red: One strike test
- Green: Detect strike, add look-ahead bonus (+2 rolls)
- Refactor: Extract
_isStrike(), clean up frame advancement - Validate: Perfect game test passes without modification!
Key Insights
Emergent Design: The algorithm structure wasn't planned upfront. Tests for simple cases forced:
- Frame-based iteration (not roll-based)
- Index tracking (advancing by 1 or 2)
- Look-ahead logic (accessing future rolls)
The Perfect Game Moment: Writing code to handle "one spare" and "one strike" automatically handled "12 consecutive strikes" (300 points). The algorithm correctly models the domain, so all valid games work.
State vs. Behavior: Initially tempting to model Frame objects with state. TDD revealed a simpler truth: just store rolls and calculate on-demand. No frame objects needed.
Comparing the Katas
Roman Numerals vs. Bowling Game
| Aspect | Roman Numerals | Bowling Game |
|---|---|---|
| Complexity | Beginner | Intermediate |
| State | Stateless transformation | Stateful (rolls accumulate) |
| Algorithm | Table-driven lookup | Frame iteration with look-ahead |
| Key Challenge | Recognizing the pattern | Managing state and bonuses |
| Design Pattern | Value Object | Strategy-like (frame types) |
| Lines of Code | ~45 production | ~30 production |
| Aha! Moment | Table eliminates duplication | Simple tests → complex games work |
What Each Kata Teaches
Roman Numerals:
- Converting domain rules into data structures
- Value Objects for enforcing constraints
- When to stop coding (algorithm emerges naturally)
Bowling Game:
- State management without over-engineering
- Look-ahead logic in sequential data
- How correct abstractions scale beyond test cases
Progressive Learning Path
- Roman Numerals first: Learn TDD fundamentals without state complexity
- Bowling Game second: Apply TDD to stateful problems
- Next kata: Choose based on what you want to practice:
- String Calculator: Parsing, validation, error handling
- Gilded Rose: Refactoring legacy code without tests
- Mars Rover: Command pattern, multiple behaviors
References
General TDD & Clean Code
- Clean Code by Robert C. Martin
- Domain-Driven Design by Eric Evans
- Test-Driven Development by Kent Beck
Kata-Specific
License
This is a learning exercise. Use freely for educational purposes.
Following The Craftsman's Way: Quality is not negotiable.