diff --git a/.gitignore b/.gitignore index 1449474..3a85790 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ # https://dart.dev/guides/libraries/private-files # Created by `dart pub` .dart_tool/ -tdd_*.md diff --git a/README.md b/README.md index d8ef3b1..e7c506b 100644 --- a/README.md +++ b/README.md @@ -219,179 +219,6 @@ gildedRose.updateQuality(); // Updates all items per domain rules --- -### 4. String Calculator ✅ - -**Implementation:** [`lib/string_calculator.dart`](lib/string_calculator.dart) -**Tests:** [`test/string_calculator_test.dart`](test/string_calculator_test.dart) - -A Bug Hunt Kata demonstrating how to use TDD to discover and fix bugs in existing code. Each bug is exposed with a RED test, then fixed with GREEN implementation. - -#### Domain Context - -A simple calculator that sums numbers from a string input with various delimiter support: - -**Features:** - -1. **Empty String:** Returns 0 -2. **Single Number:** Returns that number (`"5"` → 5) -3. **Comma Delimiter:** Sums comma-separated numbers (`"1,2,3"` → 6) -4. **Custom Delimiters:** Supports format `"//[delimiter]\n[numbers]"` (`"//;\n1;2"` → 3) -5. **Ignore Large Numbers:** Numbers > 1000 are ignored (`"2,1001"` → 2) - -#### The Bug Hunt Approach - -**Different from Previous Katas:** This wasn't built test-first. Instead, we started with **buggy working code** and used tests to expose and fix bugs one by one. - -**Bug Hunt Process:** -1. **RED:** Write test exposing a specific bug -2. **GREEN:** Fix only that bug -3. **Commit:** Document the bug found and fixed -4. **Repeat:** Move to next bug - -#### Bugs Found & Fixed - -**Bug #1: Empty String Returns Wrong Value** -- **Bug:** Returned 1 instead of 0 -- **Test:** `expect(calculator.add(''), equals(0))` -- **Fix:** Changed return value from 1 to 0 -- **Commits:** RED → GREEN - -**Bug #2: Single Number Off-By-One** -- **Bug:** Added +1 to parsed number -- **Test:** `expect(calculator.add('5'), equals(5))` -- **Expected:** 5, **Actual:** 6 -- **Fix:** Removed `+ 1` from parsing -- **Commits:** RED → GREEN - -**Bug #3: Summation Loop Misses Last Element** -- **Bug:** Loop condition `i < length - 1` skipped last item -- **Test:** `expect(calculator.add('1,2'), equals(3))` -- **Expected:** 3, **Actual:** 1 -- **Fix:** Changed to `i < length` -- **Commits:** RED → GREEN - -**Bug #4: Custom Delimiter Not Extracted** -- **Bug:** Delimiter extraction line was commented out -- **Test:** `expect(calculator.add('//;\n1;2'), equals(3))` -- **Error:** FormatException trying to parse '1;2' -- **Fix:** Uncommented `delimiter = parts[0].substring(2)` -- **Commits:** RED → GREEN - -**Bug #5: Missing Feature - Ignore Numbers > 1000** -- **Bug:** All numbers included in sum -- **Test:** `expect(calculator.add('2,1001'), equals(2))` -- **Expected:** 2, **Actual:** 1003 -- **Fix:** Added `.where((n) => n <= 1000)` filter -- **Commits:** RED → GREEN - -#### Usage - -```dart -import 'package:tdd_katas/string_calculator.dart'; - -final calculator = StringCalculator(); - -calculator.add(''); // Returns: 0 -calculator.add('5'); // Returns: 5 -calculator.add('1,2,3'); // Returns: 6 -calculator.add('//;\n1;2'); // Returns: 3 -calculator.add('2,1001'); // Returns: 2 (1001 ignored) -``` - -#### The "Aha!" Moments - -1. **Tests as Bug Detectors:** Each test acted like a spotlight, illuminating exactly ONE bug at a time. No guessing—the test tells you what's broken. - -2. **RED-GREEN Still Works:** Even when fixing bugs (not adding features), the RED-GREEN rhythm provides safety. You're never fixing blind. - -3. **Regression Prevention:** After fixing each bug, ALL previous tests stay green. This proves you didn't break something while fixing something else. - -4. **Incremental Debugging:** Fixing one bug at a time with commits creates a clear audit trail. You can see exactly what each bug was and how it was fixed. - -5. **Real-World Skill:** This mirrors production work—most code you touch is existing code with bugs, not greenfield TDD. - ---- - -### 5. Mars Rover ✅ - -**Implementation:** [`lib/mars_rover.dart`](lib/mars_rover.dart) -**Tests:** [`test/mars_rover_test.dart`](test/mars_rover_test.dart) - -A Command Pattern kata simulating a robotic rover navigating a plateau on Mars. Demonstrates clean separation of concerns, value objects, and command-based control. - -#### Domain Context - -A rover explores a rectangular plateau with coordinate-based navigation: - -**Core Concepts:** -- **Position:** (x, y) coordinates on the plateau grid -- **Direction:** Cardinal directions (N, E, S, W) -- **Plateau:** Grid with defined boundaries that wrap around (toroidal topology) - -**Commands:** -- `L` - Turn left 90 degrees (changes direction, not position) -- `R` - Turn right 90 degrees (changes direction, not position) -- `M` - Move forward one grid point in current direction - -**Example Navigation:** -``` -Starting: (0,0) facing North -Commands: "MMRMMLM" -- MM: Move to (0,2) facing North -- R: Turn to face East (still at 0,2) -- MM: Move to (2,2) facing East -- L: Turn to face North (still at 2,2) -- M: Move to (2,3) facing North -Result: (2,3) facing North -``` - -#### Key Design Decisions - -**Direction Enum:** Encapsulates rotation logic using modular arithmetic. Each direction knows how to turn left/right, eliminating conditional branching. - -**Value Objects:** -- `Position` is immutable—movement returns new position instances -- `Plateau` encapsulates boundary wrapping logic -- Prevents invalid states at the type level - -**Command Pattern (Implicit):** The `execute()` method delegates to command handlers (`turnLeft()`, `turnRight()`, `moveForward()`). Each command is isolated and testable. - -**Wrapping Logic:** Plateau boundaries wrap around (toroidal topology). Moving past edge (e.g., x=5→6 on 5x5 grid) wraps to opposite side (x=0). - -#### Usage - -```dart -import 'package:tdd_katas/mars_rover.dart'; - -// Create rover at position (1,2) facing North on 5x5 plateau -final rover = Rover( - x: 1, - y: 2, - direction: 'N', - plateauWidth: 5, - plateauHeight: 5, -); - -rover.execute('LMLMLMLMM'); - -print('Position: (${rover.x}, ${rover.y})'); // Position: (1, 3) -print('Direction: ${rover.direction}'); // Direction: N -``` - -#### The "Aha!" Moments - -1. **Enums as Behavior Carriers:** Direction enum doesn't just store values—it encapsulates rotation logic. Turning left/right becomes `direction.turnLeft()`, eliminating lookup tables. - -2. **Value Objects Prevent Bugs:** Immutable `Position` means movement can't corrupt state. New position calculated, validated, then assigned. Boundary wrapping isolated in `Plateau`. - -3. **Switch Expressions Shine:** Modern Dart's `switch` expression makes direction-based movement elegant and exhaustive. Compiler enforces handling all directions. - -4. **Modular Arithmetic for Rotation:** `(index + 1) % 4` handles right rotation elegantly. No if-statements, no edge cases—math models the domain perfectly. - -5. **Refactoring Without Fear:** Two refactoring commits drastically improved code structure. Tests stayed green throughout, proving behavior preservation. - ---- - ## Running Tests ```bash @@ -402,8 +229,6 @@ dart test dart test test/roman_numerals_test.dart dart test test/bowling_game_test.dart dart test test/gilded_rose_test.dart -dart test test/string_calculator_test.dart -dart test test/mars_rover_test.dart # Run with coverage dart test --coverage @@ -711,23 +536,22 @@ Decision: Skip refactor commit—code is already excellent. ## Comparing the Katas -### All Five Katas at a Glance +### All Three Katas at a Glance -| Aspect | Roman Numerals | Bowling Game | Gilded Rose | String Calculator | Mars Rover | -|--------|----------------|--------------|-------------|-------------------|------------| -| **Complexity** | Beginner | Intermediate | Advanced | Beginner | Intermediate | -| **Approach** | Greenfield TDD | Greenfield TDD | Legacy refactoring | Bug hunting | Greenfield TDD | -| **State** | Stateless | Stateful | Stateful | Stateless | Stateful | -| **Algorithm** | Table-driven | Frame iteration | Strategy pattern | String parsing | Command pattern | -| **Key Challenge** | Pattern recognition | State & bonuses | Refactoring safely | Finding bugs | Navigation & wrapping | -| **Design Pattern** | Value Object | Implicit strategy | Explicit strategy | Filters & pipes | Command + Value Objects | -| **Lines of Code** | ~45 production | ~30 production | ~120 production | ~25 production | ~95 production | -| **Test Count** | ~15 tests | ~10 tests | ~17 tests | 6 tests | 23 tests | -| **Aha! Moment** | Table = data | Simple → complex | Refactor = safe | Tests find bugs | Enums carry behavior | +| 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:** **Roman Numerals:** - Converting domain rules into data structures - Value Objects for enforcing constraints @@ -737,37 +561,22 @@ Decision: Skip refactor commit—code is already excellent. - 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 -**String Calculator:** -- Using tests to expose bugs in existing code -- Bug hunting with RED-GREEN discipline -- Incremental debugging with clear commits -- Regression prevention through test accumulation - -**Mars Rover:** -- Command pattern for behavior delegation -- Value Objects for domain modeling (Position, Plateau, Direction) -- Enums as behavior carriers, not just constants -- Coordinate systems and wrapping logic -- Progressive refactoring with confidence - ### Progressive Learning Path 1. **Roman Numerals first:** Learn TDD fundamentals without state complexity 2. **Bowling Game second:** Apply TDD to stateful problems -3. **Mars Rover third:** Master Command pattern and value objects -4. **Gilded Rose fourth:** Refactor legacy code with tests as safety net -5. **String Calculator fifth:** Practice bug hunting and fixing with TDD -6. **Next kata:** Choose based on what you want to practice: - - **Prime Factors:** Mathematical decomposition, algorithmic thinking - - **Tennis Scoring:** State machines, domain language - - **FizzBuzz:** Classic conditional logic exercise +3. **Gilded Rose third:** Master refactoring legacy code with tests as safety net +4. **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 --- diff --git a/bin/tdd_katas.dart b/bin/tdd_katas.dart index 24f0e29..987a845 100644 --- a/bin/tdd_katas.dart +++ b/bin/tdd_katas.dart @@ -1,3 +1,5 @@ +import 'package:tdd_katas/roman_numerals.dart' as roman_numerals; + void main(List arguments) { - print('TDD Katas exercises, please read the README.md file.'); + print('Hello world: ${roman_numerals.integerToRoman(1)}'); } diff --git a/lib/gilded_rose.dart b/lib/gilded_rose.dart index 6a813d9..01c7722 100644 --- a/lib/gilded_rose.dart +++ b/lib/gilded_rose.dart @@ -14,6 +14,7 @@ class Item { /// Quality bounds - domain constraints const int _minQuality = 0; const int _maxQuality = 50; +const int _legendaryQuality = 80; // unused, just for documentation /// Helper methods for quality management void _degradeQuality(Item item, int amount) { diff --git a/lib/mars_rover.dart b/lib/mars_rover.dart deleted file mode 100644 index 221e450..0000000 --- a/lib/mars_rover.dart +++ /dev/null @@ -1,106 +0,0 @@ -enum Direction { - north('N'), - east('E'), - south('S'), - west('W'); - - final String code; - const Direction(this.code); - - static Direction fromCode(String code) { - return Direction.values.firstWhere((d) => d.code == code); - } - - Direction turnLeft() { - return Direction.values[(index + 3) % 4]; - } - - Direction turnRight() { - return Direction.values[(index + 1) % 4]; - } -} - -class Position { - final int x; - final int y; - - Position(this.x, this.y); - - Position moveNorth() => Position(x, y + 1); - Position moveEast() => Position(x + 1, y); - Position moveSouth() => Position(x, y - 1); - Position moveWest() => Position(x - 1, y); -} - -class Plateau { - final int width; - final int height; - - Plateau(this.width, this.height); - - Position wrap(Position position) { - final wrappedX = _wrapCoordinate(position.x, width); - final wrappedY = _wrapCoordinate(position.y, height); - return Position(wrappedX, wrappedY); - } - - int _wrapCoordinate(int value, int max) { - final wrapped = value % (max + 1); - return wrapped < 0 ? wrapped + max + 1 : wrapped; - } -} - -class Rover { - Position _position; - Direction _direction; - final Plateau plateau; - - Rover({ - required int x, - required int y, - required String direction, - int plateauWidth = 100, - int plateauHeight = 100, - }) : _position = Position(x, y), - _direction = Direction.fromCode(direction), - plateau = Plateau(plateauWidth, plateauHeight); - - int get x => _position.x; - int get y => _position.y; - String get direction => _direction.code; - - void turnLeft() { - _direction = _direction.turnLeft(); - } - - void turnRight() { - _direction = _direction.turnRight(); - } - - void moveForward() { - final newPosition = switch (_direction) { - Direction.north => _position.moveNorth(), - Direction.east => _position.moveEast(), - Direction.south => _position.moveSouth(), - Direction.west => _position.moveWest(), - }; - _position = plateau.wrap(newPosition); - } - - void execute(String commands) { - for (var i = 0; i < commands.length; i++) { - final command = commands[i]; - switch (command) { - case 'L': - turnLeft(); - break; - case 'R': - turnRight(); - break; - case 'M': - moveForward(); - break; - } - } - } -} diff --git a/lib/string_calculator.dart b/lib/string_calculator.dart deleted file mode 100644 index 6e5ec31..0000000 --- a/lib/string_calculator.dart +++ /dev/null @@ -1,55 +0,0 @@ -/// String Calculator - Buggy Implementation -/// This code has intentional bugs for the Bug Hunt Kata exercise -/// -/// Requirements: -/// 1. Empty string returns 0 -/// 2. Single number returns that number -/// 3. Two numbers comma-delimited returns sum -/// 4. Handle newlines as delimiters -/// 5. Support custom delimiters: "//[delimiter]\n[numbers]" -library; - -class StringCalculator { - int add(String numbers) { - // Bug 1: Empty string handling - if (numbers.isEmpty) { - return 0; // Fixed: Return 0 for empty string - } - - // Bug 2: Single number parsing - if (!numbers.contains(',') && - !numbers.contains('\n') && - !numbers.startsWith('//')) { - return int.parse(numbers); // Fixed: Removed off-by-one error - } - - String delimiter = ','; - String numbersToProcess = numbers; - - // Custom delimiter support - if (numbers.startsWith('//')) { - // Bug 4: Custom delimiter not actually used - final parts = numbers.split('\n'); - delimiter = parts[0].substring(2); // Fixed: Extract custom delimiter - numbersToProcess = parts.skip(1).join('\n'); - } - - // Bug 3 & 4: Delimiter handling issues - final numList = numbersToProcess - .replaceAll('\n', delimiter) - .split(delimiter) - .where((s) => s.isNotEmpty) - .map((s) => int.parse(s)) - .where((n) => n <= 1000) // Filter out numbers > 1000 - .toList(); - - // Bug 3: Off-by-one in summation - int sum = 0; - for (int i = 0; i < numList.length; i++) { - // Fixed: Include last element - sum += numList[i]; - } - - return sum; - } -} diff --git a/test/mars_rover_test.dart b/test/mars_rover_test.dart deleted file mode 100644 index 53e4540..0000000 --- a/test/mars_rover_test.dart +++ /dev/null @@ -1,194 +0,0 @@ -import 'package:tdd_katas/mars_rover.dart'; -import 'package:test/test.dart'; - -void main() { - group('Mars Rover:', () { - test('rover reports initial position and direction', () { - final rover = Rover(x: 0, y: 0, direction: 'N'); - - expect(rover.x, equals(0)); - expect(rover.y, equals(0)); - expect(rover.direction, equals('N')); - }); - - group('Turning Left:', () { - test('from North faces West', () { - final rover = Rover(x: 0, y: 0, direction: 'N'); - rover.turnLeft(); - expect(rover.direction, equals('W')); - }); - - test('from West faces South', () { - final rover = Rover(x: 0, y: 0, direction: 'W'); - rover.turnLeft(); - expect(rover.direction, equals('S')); - }); - - test('from South faces East', () { - final rover = Rover(x: 0, y: 0, direction: 'S'); - rover.turnLeft(); - expect(rover.direction, equals('E')); - }); - - test('from East faces North', () { - final rover = Rover(x: 0, y: 0, direction: 'E'); - rover.turnLeft(); - expect(rover.direction, equals('N')); - }); - }); - - group('Turning Right:', () { - test('from North faces East', () { - final rover = Rover(x: 0, y: 0, direction: 'N'); - rover.turnRight(); - expect(rover.direction, equals('E')); - }); - - test('from East faces South', () { - final rover = Rover(x: 0, y: 0, direction: 'E'); - rover.turnRight(); - expect(rover.direction, equals('S')); - }); - - test('from South faces West', () { - final rover = Rover(x: 0, y: 0, direction: 'S'); - rover.turnRight(); - expect(rover.direction, equals('W')); - }); - - test('from West faces North', () { - final rover = Rover(x: 0, y: 0, direction: 'W'); - rover.turnRight(); - expect(rover.direction, equals('N')); - }); - }); - - group('Moving Forward:', () { - test('facing North increases Y', () { - final rover = Rover(x: 0, y: 0, direction: 'N'); - rover.moveForward(); - expect(rover.x, equals(0)); - expect(rover.y, equals(1)); - }); - - test('facing East increases X', () { - final rover = Rover(x: 0, y: 0, direction: 'E'); - rover.moveForward(); - expect(rover.x, equals(1)); - expect(rover.y, equals(0)); - }); - - test('facing South decreases Y', () { - final rover = Rover(x: 0, y: 5, direction: 'S'); - rover.moveForward(); - expect(rover.x, equals(0)); - expect(rover.y, equals(4)); - }); - - test('facing West decreases X', () { - final rover = Rover(x: 5, y: 0, direction: 'W'); - rover.moveForward(); - expect(rover.x, equals(4)); - expect(rover.y, equals(0)); - }); - }); - - group('Executing Command Sequences:', () { - test('single command L', () { - final rover = Rover(x: 0, y: 0, direction: 'N'); - rover.execute('L'); - expect(rover.direction, equals('W')); - }); - - test('single command R', () { - final rover = Rover(x: 0, y: 0, direction: 'N'); - rover.execute('R'); - expect(rover.direction, equals('E')); - }); - - test('single command M', () { - final rover = Rover(x: 0, y: 0, direction: 'N'); - rover.execute('M'); - expect(rover.y, equals(1)); - }); - - test('complex sequence MMRMMLM', () { - final rover = Rover(x: 0, y: 0, direction: 'N'); - rover.execute('MMRMMLM'); - expect(rover.x, equals(2)); - expect(rover.y, equals(3)); - expect(rover.direction, equals('N')); - }); - - test('example from Mars Rover kata', () { - final rover = Rover(x: 1, y: 2, direction: 'N'); - rover.execute('LMLMLMLMM'); - expect(rover.x, equals(1)); - expect(rover.y, equals(3)); - expect(rover.direction, equals('N')); - }); - }); - - group('Plateau Boundaries:', () { - test('wraps around when moving North past boundary', () { - final rover = Rover( - x: 0, - y: 5, - direction: 'N', - plateauWidth: 5, - plateauHeight: 5, - ); - rover.moveForward(); - expect(rover.y, equals(0)); - }); - - test('wraps around when moving East past boundary', () { - final rover = Rover( - x: 5, - y: 0, - direction: 'E', - plateauWidth: 5, - plateauHeight: 5, - ); - rover.moveForward(); - expect(rover.x, equals(0)); - }); - - test('wraps around when moving South past boundary', () { - final rover = Rover( - x: 0, - y: 0, - direction: 'S', - plateauWidth: 5, - plateauHeight: 5, - ); - rover.moveForward(); - expect(rover.y, equals(5)); - }); - - test('wraps around when moving West past boundary', () { - final rover = Rover( - x: 0, - y: 0, - direction: 'W', - plateauWidth: 5, - plateauHeight: 5, - ); - rover.moveForward(); - expect(rover.x, equals(5)); - }); - - test('example with wrapping', () { - final rover = Rover( - x: 5, - y: 5, - direction: 'N', - plateauWidth: 5, - plateauHeight: 5, - ); - rover.execute('MMM'); - expect(rover.y, equals(2)); - }); - }); - }); -} diff --git a/test/string_calculator_test.dart b/test/string_calculator_test.dart deleted file mode 100644 index 1d4ee44..0000000 --- a/test/string_calculator_test.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:tdd_katas/string_calculator.dart'; -import 'package:test/test.dart'; - -void main() { - group('String Calculator - Bug Hunt', () { - late StringCalculator calculator; - - setUp(() { - calculator = StringCalculator(); - }); - - test('empty string returns 0', () { - expect(calculator.add(''), equals(0)); - }); - - test('single number returns that number', () { - expect(calculator.add('5'), equals(5)); - expect(calculator.add('42'), equals(42)); - }); - - test('two comma-delimited numbers return sum', () { - expect(calculator.add('1,2'), equals(3)); - expect(calculator.add('10,20'), equals(30)); - }); - - test('multiple comma-delimited numbers return sum', () { - expect(calculator.add('1,2,3'), equals(6)); - expect(calculator.add('5,10,15,20'), equals(50)); - }); - - test('custom delimiter works correctly', () { - expect(calculator.add('//;\n1;2'), equals(3)); - expect(calculator.add('//|\n10|20|30'), equals(60)); - }); - - test('numbers greater than 1000 are ignored', () { - expect(calculator.add('2,1001'), equals(2)); - expect(calculator.add('1000,1001,2'), equals(1002)); - }); - }); -}