- Added .where((n) => n <= 1000) filter - Numbers greater than 1000 now excluded from sum - Tests pass: '2,1001' returns 2, '1000,1001,2' returns 1002 |
||
|---|---|---|
| bin | ||
| lib | ||
| test | ||
| .gitignore | ||
| analysis_options.yaml | ||
| CHANGELOG.md | ||
| pubspec.lock | ||
| pubspec.yaml | ||
| README.md | ||
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. Gilded Rose ✅
Implementation: lib/gilded_rose.dart
Tests: test/gilded_rose_test.dart
A legacy code refactoring kata demonstrating how to safely transform deeply nested conditionals into clean, extensible code using characterization tests and the Strategy pattern.
Domain Context
An inn's inventory system that updates item quality daily based on complex business rules:
Item Types:
- Normal Items: Quality decreases by 1/day, 2/day after sell-by date
- Aged Brie: Quality increases by 1/day, 2/day after expiration (improves with age!)
- Sulfuras (Legendary): Never changes, quality always 80, never "expires"
- Backstage Passes: Complex appreciation:
- More than 10 days: +1 quality/day
- 10 days or less: +2 quality/day
- 5 days or less: +3 quality/day
- After concert (sellIn < 0): Quality drops to 0
- Conjured Items: Degrade twice as fast as normal items (2/day, 4/day after expiration)
Domain Constraints:
- Quality never negative (≥ 0)
- Quality never exceeds 50 (except Sulfuras at 80)
- Cannot modify the
Itemclass (goblin constraint!)
Key Design Decisions
Characterization Testing: Before touching legacy code, created 17 tests to capture existing behavior as a "safety net." Includes a Golden Master test simulating 30 days.
Strategy Pattern: Each item type gets its own updater class implementing ItemUpdater interface. Eliminates conditional branching and enables Open-Closed Principle.
Helper Methods & Constants: _degradeQuality() and _improveQuality() with automatic clamping eliminate scattered boundary checks. Domain constants (_minQuality, _maxQuality) remove magic numbers.
Factory Pattern: _selectUpdater() method chooses the appropriate strategy based on item name, enabling polymorphic dispatch.
The Refactoring Journey
Phase 1: Understand Legacy Code
// 60+ lines of 7-8 level nested conditionals
// Nearly incomprehensible logic mixing all item types
Phase 2: Characterization Tests (GREEN Phase)
- 14 comprehensive tests covering all item types
- Golden Master test: 30-day simulation baseline
- Result: Safety net established ✅
Phase 3: Refactor with Confidence (3 steps)
Step 1 - Extract Methods (REFACTOR):
// Before: 60 lines of nested hell
// After: Clean if-else delegating to 4 private methods
_updateNormalItem(), _updateAgedBrie(),
_updateBackstagePasses(), _updateSulfuras()
Step 2 - Strategy Pattern (REFACTOR):
abstract class ItemUpdater {
void update(Item item);
}
class NormalItemUpdater implements ItemUpdater { ... }
class AgedBrieUpdater implements ItemUpdater { ... }
// ... 4 concrete strategies
Step 3 - Domain Helpers (REFACTOR):
const int _minQuality = 0;
const int _maxQuality = 50;
void _degradeQuality(Item item, int amount) {
item.quality = (item.quality - amount).clamp(_minQuality, _maxQuality);
}
Phase 4: Add Conjured Items (RED-GREEN)
RED: Added 3 failing tests for Conjured items behavior
GREEN: Created ConjuredItemUpdater class—one class, one condition
Result: Feature added in minutes thanks to refactoring!
Usage
import 'package:tdd_katas/gilded_rose.dart';
final items = [
Item('Normal Sword', 10, 20),
Item('Aged Brie', 2, 0),
Item('Sulfuras, Hand of Ragnaros', 0, 80),
Item('Backstage passes to a TAFKAL80ETC concert', 15, 20),
Item('Conjured Mana Cake', 3, 6),
];
final gildedRose = GildedRose(items);
gildedRose.updateQuality(); // Updates all items per domain rules
The "Aha!" Moments
-
Characterization Tests = Freedom: With tests in place, aggressive refactoring felt safe. Every change validated instantly.
-
Strategy Pattern = Extensibility: Adding Conjured items took 5 minutes. Before refactoring, it would have meant diving into nested conditionals and risking bugs.
-
Small Steps = Big Wins: Three refactoring commits transformed spaghetti into clean code. Each step kept tests GREEN, proving behavior preservation.
-
Open-Closed Principle in Action: New item types don't modify existing code—they just add new updater classes. The system is "open for extension, closed for modification."
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
dart test test/gilded_rose_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.
Gilded Rose: Detailed Journey
The Legacy Code Challenge
Unlike the previous two katas (greenfield TDD), Gilded Rose simulates real-world legacy code refactoring. You inherit messy, working code with no tests and must:
- Understand what it does (without breaking it)
- Add tests to capture behavior
- Refactor safely
- Add new features
Test Strategy
Tests are organized by item type behavior and include a Golden Master:
Normal Items:
- Quality degradation (1/day, 2/day after expiration)
- Quality never negative
Aged Brie:
- Quality appreciation (improves with age)
- Respects quality cap (≤ 50)
Sulfuras (Legendary Items):
- Never changes (quality, sellIn)
- Always quality 80
Backstage Passes:
- Threshold-based appreciation (10 days, 5 days)
- Drops to 0 after concert
Conjured Items (new feature):
- Degrades 2x faster than normal items
Golden Master Test:
- 30-day simulation with all item types
- Captures baseline output before refactoring
- Detects any behavioral regression
Refactoring Timeline
Phase 1: Create Legacy Code
- Intentionally nested 7-8 levels deep
- Mixed concerns (all item types in one method)
- Magic numbers scattered throughout
- Result: Represents realistic legacy code
Phase 2: Characterization Tests (GREEN)
- 14 comprehensive tests written before any refactoring
- Golden Master baseline captured
- Commit:
"GREEN: Add characterization tests" - Result: Safety net established ✅
Phase 3: Refactor in Small Steps (3 REFACTOR commits)
Step 1 - Extract Methods:
// Before: 60 lines, items[i] everywhere, deeply nested
for (var i = 0; i < items.length; i++) {
if (items[i].name != 'Aged Brie' && ...) {
if (items[i].quality > 0) {
if (items[i].name != 'Sulfuras...') {
// ... 5 more levels ...
// After: Clean delegation, readable
for (final item in items) {
if (item.name == 'Sulfuras, Hand of Ragnaros') {
_updateSulfuras(item);
} else if (item.name == 'Aged Brie') {
_updateAgedBrie(item);
// ...
}
Commit: "REFACTOR: Extract item type methods from nested conditionals"
Step 2 - Introduce Strategy Pattern:
abstract class ItemUpdater {
void update(Item item);
}
class NormalItemUpdater implements ItemUpdater {
@override
void update(Item item) {
_degradeQuality(item, 1);
item.sellIn -= 1;
if (item.sellIn < 0) {
_degradeQuality(item, 1);
}
}
}
// ... 4 concrete strategies
ItemUpdater _selectUpdater(Item item) { ... }
Commit: "REFACTOR: Introduce Strategy pattern for item types"
Step 3 - Extract Domain Helpers:
const int _minQuality = 0;
const int _maxQuality = 50;
void _degradeQuality(Item item, int amount) {
item.quality = (item.quality - amount).clamp(_minQuality, _maxQuality);
}
void _improveQuality(Item item, int amount) {
item.quality = (item.quality + amount).clamp(_minQuality, _maxQuality);
}
Commit: "REFACTOR: Extract helper methods and domain constants"
All 14 tests stayed GREEN throughout! 🟢
Phase 4: Add Conjured Items (RED-GREEN-REFACTOR)
RED: Write failing tests
test('degrade in quality twice as fast as normal items', () {
final items = [Item('Conjured Mana Cake', 10, 20)];
GildedRose(items).updateQuality();
expect(items[0].quality, equals(18)); // -2 instead of -1
});
Result: 2 tests FAILING ❌ (Expected: 18, Actual: 19)
Commit: "RED: Add failing tests for Conjured items"
GREEN: Implement minimal solution
class ConjuredItemUpdater implements ItemUpdater {
@override
void update(Item item) {
_degradeQuality(item, 2); // 2x normal rate
item.sellIn -= 1;
if (item.sellIn < 0) {
_degradeQuality(item, 2); // 4x total after expiration
}
}
}
ItemUpdater _selectUpdater(Item item) {
// ... existing checks ...
} else if (item.name.startsWith('Conjured')) {
return ConjuredItemUpdater();
} else {
return NormalItemUpdater();
}
}
Result: All 17 tests PASSING ✅
Commit: "GREEN: Implement Conjured items degrading twice as fast"
REFACTOR: Already clean! No duplication, clear names, reusing helpers.
Decision: Skip refactor commit—code is already excellent.
Development Metrics
| Metric | Before Refactoring | After Refactoring |
|---|---|---|
| Cyclomatic Complexity | ~25 (very high) | ~3 per class (low) |
| Lines per Method | 60+ | 5-10 |
| Max Nesting | 7-8 levels | 2 levels |
| Time to Add Feature | Hours (risky) | Minutes (safe) |
| Test Coverage | 0% → 100% | 100% (maintained) |
Key Insights
Characterization Tests Are Your Lifeline: Without tests, refactoring is guesswork. With tests, it's engineering. Every change validated in milliseconds.
Small Steps = Low Risk: Three refactoring commits, each preserving behavior. No "big bang" rewrite—steady, safe progress.
Strategy Pattern = Future-Proofing: Adding Conjured items demonstrated the payoff:
- Before refactoring: Would require diving into nested conditionals, risking bugs
- After refactoring: One new class, one condition, done in 5 minutes
Golden Master Testing: The 30-day simulation test caught edge cases that individual unit tests missed. It serves as a comprehensive regression detector.
Open-Closed Principle Validated: New item types extend the system without modifying existing updater classes. The design is "open for extension, closed for modification."
Refactoring ≠ Rewriting: We never changed what the code does, only how it's structured. Tests prove behavioral equivalence at every step.
Comparing the Katas
All Three Katas at a Glance
| Aspect | Roman Numerals | Bowling Game | Gilded Rose |
|---|---|---|---|
| Complexity | Beginner | Intermediate | Advanced |
| Approach | Greenfield TDD | Greenfield TDD | Legacy refactoring |
| State | Stateless | Stateful | Stateful |
| Algorithm | Table-driven lookup | Frame iteration | Strategy pattern |
| Key Challenge | Pattern recognition | State & bonuses | Refactoring safely |
| Design Pattern | Value Object | Implicit strategy | Explicit strategy |
| Lines of Code | ~45 production | ~30 production | ~120 production |
| Test Count | ~15 tests | ~10 tests | ~17 tests |
| Aha! Moment | Table = data structure | Simple → complex works | Refactoring = safety |
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 Gilded Rose:
- Safely refactoring legacy code with characterization tests
- Strategy pattern for eliminating conditional complexity
- Open-Closed Principle for extensibility
- Working effectively with code you didn't write
Progressive Learning Path
- Roman Numerals first: Learn TDD fundamentals without state complexity
- Bowling Game second: Apply TDD to stateful problems
- Gilded Rose third: Master refactoring legacy code with tests as safety net
- Next kata: Choose based on what you want to practice:
- String Calculator: Parsing, validation, error handling
- Mars Rover: Command pattern, multiple behaviors
- Prime Factors: Mathematical decomposition, algorithmic thinkingsts
- Mars Rover: Command pattern, multiple behaviors
References
General TDD & Clean Code
- Gilded Rose Kata
- 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.