From 7f05229104373d4a5ed8a9c7929a402b5b61fa66 Mon Sep 17 00:00:00 2001 From: fiatcode Date: Wed, 18 Feb 2026 12:06:26 +0700 Subject: [PATCH 1/7] GREEN: add characterization tests for legacy Gilded Rose code --- lib/gilded_rose.dart | 71 ++++ test/gilded_rose_test.dart | 727 +++++++++++++++++++++++++++++++++++++ 2 files changed, 798 insertions(+) create mode 100644 lib/gilded_rose.dart create mode 100644 test/gilded_rose_test.dart diff --git a/lib/gilded_rose.dart b/lib/gilded_rose.dart new file mode 100644 index 0000000..5c87d3a --- /dev/null +++ b/lib/gilded_rose.dart @@ -0,0 +1,71 @@ +// WARNING: DO NOT modify the Item class - the goblin in the corner will insta-rage! + +class Item { + String name; + int sellIn; + int quality; + + Item(this.name, this.sellIn, this.quality); + + @override + String toString() => '$name, $sellIn, $quality'; +} + +class GildedRose { + List items; + + GildedRose(this.items); + + void updateQuality() { + for (var i = 0; i < items.length; i++) { + if (items[i].name != 'Aged Brie' && + items[i].name != 'Backstage passes to a TAFKAL80ETC concert') { + if (items[i].quality > 0) { + if (items[i].name != 'Sulfuras, Hand of Ragnaros') { + items[i].quality = items[i].quality - 1; + } + } + } else { + if (items[i].quality < 50) { + items[i].quality = items[i].quality + 1; + + if (items[i].name == 'Backstage passes to a TAFKAL80ETC concert') { + if (items[i].sellIn < 11) { + if (items[i].quality < 50) { + items[i].quality = items[i].quality + 1; + } + } + + if (items[i].sellIn < 6) { + if (items[i].quality < 50) { + items[i].quality = items[i].quality + 1; + } + } + } + } + } + + if (items[i].name != 'Sulfuras, Hand of Ragnaros') { + items[i].sellIn = items[i].sellIn - 1; + } + + if (items[i].sellIn < 0) { + if (items[i].name != 'Aged Brie') { + if (items[i].name != 'Backstage passes to a TAFKAL80ETC concert') { + if (items[i].quality > 0) { + if (items[i].name != 'Sulfuras, Hand of Ragnaros') { + items[i].quality = items[i].quality - 1; + } + } + } else { + items[i].quality = items[i].quality - items[i].quality; + } + } else { + if (items[i].quality < 50) { + items[i].quality = items[i].quality + 1; + } + } + } + } + } +} diff --git a/test/gilded_rose_test.dart b/test/gilded_rose_test.dart new file mode 100644 index 0000000..fbe0ed9 --- /dev/null +++ b/test/gilded_rose_test.dart @@ -0,0 +1,727 @@ +import 'package:tdd_katas/gilded_rose.dart'; +import 'package:test/test.dart'; + +void main() { + group('Gilded Rose Inn - Characterization Tests', () { + group('Normal items', () { + test('quality and sellIn both decrease each day', () { + final items = [Item('Normal Item', 10, 20)]; + final gildedRose = GildedRose(items); + + gildedRose.updateQuality(); + + expect(items[0].sellIn, equals(9)); + expect(items[0].quality, equals(19)); + }); + + test('quality degrades twice as fast after sell-by date', () { + final items = [Item('Normal Item', 0, 10)]; + final gildedRose = GildedRose(items); + + gildedRose.updateQuality(); + + expect(items[0].sellIn, equals(-1)); + expect(items[0].quality, equals(8)); // -2 quality + }); + + test('quality never becomes negative', () { + final items = [Item('Normal Item', 5, 0)]; + final gildedRose = GildedRose(items); + + gildedRose.updateQuality(); + + expect(items[0].quality, equals(0)); + }); + }); + + group('Aged Brie - appreciates over time', () { + test('quality increases as it ages', () { + final items = [Item('Aged Brie', 10, 20)]; + final gildedRose = GildedRose(items); + + gildedRose.updateQuality(); + + expect(items[0].sellIn, equals(9)); + expect(items[0].quality, equals(21)); + }); + + test('quality increases twice as fast after sell-by date', () { + final items = [Item('Aged Brie', 0, 20)]; + final gildedRose = GildedRose(items); + + gildedRose.updateQuality(); + + expect(items[0].sellIn, equals(-1)); + expect(items[0].quality, equals(22)); // +2 quality + }); + + test('quality never exceeds 50', () { + final items = [Item('Aged Brie', 5, 50)]; + final gildedRose = GildedRose(items); + + gildedRose.updateQuality(); + + expect(items[0].quality, equals(50)); + }); + }); + + group('Sulfuras - legendary item', () { + test('quality never changes', () { + final items = [Item('Sulfuras, Hand of Ragnaros', 10, 80)]; + final gildedRose = GildedRose(items); + + gildedRose.updateQuality(); + + expect(items[0].quality, equals(80)); + }); + + test('sellIn never decreases', () { + final items = [Item('Sulfuras, Hand of Ragnaros', 10, 80)]; + final gildedRose = GildedRose(items); + + gildedRose.updateQuality(); + + expect(items[0].sellIn, equals(10)); + }); + }); + + group('Backstage passes - concert tickets', () { + test('quality increases by 1 when more than 10 days away', () { + final items = [ + Item('Backstage passes to a TAFKAL80ETC concert', 15, 20), + ]; + final gildedRose = GildedRose(items); + + gildedRose.updateQuality(); + + expect(items[0].quality, equals(21)); // +1 + }); + + test('quality increases by 2 when 10 days or less', () { + final items = [ + Item('Backstage passes to a TAFKAL80ETC concert', 10, 20), + ]; + final gildedRose = GildedRose(items); + + gildedRose.updateQuality(); + + expect(items[0].quality, equals(22)); // +2 + }); + + test('quality increases by 3 when 5 days or less', () { + final items = [ + Item('Backstage passes to a TAFKAL80ETC concert', 5, 20), + ]; + final gildedRose = GildedRose(items); + + gildedRose.updateQuality(); + + expect(items[0].quality, equals(23)); // +3 + }); + + test('quality drops to 0 after concert', () { + final items = [ + Item('Backstage passes to a TAFKAL80ETC concert', 0, 20), + ]; + final gildedRose = GildedRose(items); + + gildedRose.updateQuality(); + + expect(items[0].sellIn, equals(-1)); + expect(items[0].quality, equals(0)); + }); + + test('quality never exceeds 50', () { + final items = [ + Item('Backstage passes to a TAFKAL80ETC concert', 5, 49), + ]; + final gildedRose = GildedRose(items); + + gildedRose.updateQuality(); + + expect(items[0].quality, equals(50)); // capped at 50 + }); + }); + + group('Golden Master - 30-day simulation', () { + test('captures complete behavior of all item types over time', () { + final items = _createAllItemTypes(); + final gildedRose = GildedRose(items); + final output = StringBuffer(); + + for (var day = 0; day <= 30; day++) { + output.writeln('-------- day $day --------'); + output.writeln('name, sellIn, quality'); + for (final item in items) { + output.writeln(item); + } + output.writeln(); + gildedRose.updateQuality(); + } + + final expected = _goldenMasterBaseline(); + expect( + output.toString(), + equals(expected), + reason: 'Golden Master mismatch - legacy behavior changed!', + ); + }); + }); + }); +} + +List _createAllItemTypes() { + return [ + // Normal items at various states + Item('Normal Item', 10, 20), + Item('Normal Item', 2, 5), + Item('Normal Item', 0, 10), + Item('Normal Item', -1, 10), + // Aged Brie + Item('Aged Brie', 10, 20), + Item('Aged Brie', 0, 30), + Item('Aged Brie', 5, 49), + // Sulfuras + Item('Sulfuras, Hand of Ragnaros', 10, 80), + Item('Sulfuras, Hand of Ragnaros', -1, 80), + // Backstage passes at critical thresholds + Item('Backstage passes to a TAFKAL80ETC concert', 15, 20), + Item('Backstage passes to a TAFKAL80ETC concert', 10, 20), + Item('Backstage passes to a TAFKAL80ETC concert', 5, 20), + Item('Backstage passes to a TAFKAL80ETC concert', 1, 20), + Item('Backstage passes to a TAFKAL80ETC concert', 0, 20), + ]; +} + +String _goldenMasterBaseline() { + // Generated by running the legacy code + // This captures the exact behavior - our safety net! + return '''-------- day 0 -------- +name, sellIn, quality +Normal Item, 10, 20 +Normal Item, 2, 5 +Normal Item, 0, 10 +Normal Item, -1, 10 +Aged Brie, 10, 20 +Aged Brie, 0, 30 +Aged Brie, 5, 49 +Sulfuras, Hand of Ragnaros, 10, 80 +Sulfuras, Hand of Ragnaros, -1, 80 +Backstage passes to a TAFKAL80ETC concert, 15, 20 +Backstage passes to a TAFKAL80ETC concert, 10, 20 +Backstage passes to a TAFKAL80ETC concert, 5, 20 +Backstage passes to a TAFKAL80ETC concert, 1, 20 +Backstage passes to a TAFKAL80ETC concert, 0, 20 + +-------- day 1 -------- +name, sellIn, quality +Normal Item, 9, 19 +Normal Item, 1, 4 +Normal Item, -1, 8 +Normal Item, -2, 8 +Aged Brie, 9, 21 +Aged Brie, -1, 32 +Aged Brie, 4, 50 +Sulfuras, Hand of Ragnaros, 10, 80 +Sulfuras, Hand of Ragnaros, -1, 80 +Backstage passes to a TAFKAL80ETC concert, 14, 21 +Backstage passes to a TAFKAL80ETC concert, 9, 22 +Backstage passes to a TAFKAL80ETC concert, 4, 23 +Backstage passes to a TAFKAL80ETC concert, 0, 23 +Backstage passes to a TAFKAL80ETC concert, -1, 0 + +-------- day 2 -------- +name, sellIn, quality +Normal Item, 8, 18 +Normal Item, 0, 3 +Normal Item, -2, 6 +Normal Item, -3, 6 +Aged Brie, 8, 22 +Aged Brie, -2, 34 +Aged Brie, 3, 50 +Sulfuras, Hand of Ragnaros, 10, 80 +Sulfuras, Hand of Ragnaros, -1, 80 +Backstage passes to a TAFKAL80ETC concert, 13, 22 +Backstage passes to a TAFKAL80ETC concert, 8, 24 +Backstage passes to a TAFKAL80ETC concert, 3, 26 +Backstage passes to a TAFKAL80ETC concert, -1, 0 +Backstage passes to a TAFKAL80ETC concert, -2, 0 + +-------- day 3 -------- +name, sellIn, quality +Normal Item, 7, 17 +Normal Item, -1, 1 +Normal Item, -3, 4 +Normal Item, -4, 4 +Aged Brie, 7, 23 +Aged Brie, -3, 36 +Aged Brie, 2, 50 +Sulfuras, Hand of Ragnaros, 10, 80 +Sulfuras, Hand of Ragnaros, -1, 80 +Backstage passes to a TAFKAL80ETC concert, 12, 23 +Backstage passes to a TAFKAL80ETC concert, 7, 26 +Backstage passes to a TAFKAL80ETC concert, 2, 29 +Backstage passes to a TAFKAL80ETC concert, -2, 0 +Backstage passes to a TAFKAL80ETC concert, -3, 0 + +-------- day 4 -------- +name, sellIn, quality +Normal Item, 6, 16 +Normal Item, -2, 0 +Normal Item, -4, 2 +Normal Item, -5, 2 +Aged Brie, 6, 24 +Aged Brie, -4, 38 +Aged Brie, 1, 50 +Sulfuras, Hand of Ragnaros, 10, 80 +Sulfuras, Hand of Ragnaros, -1, 80 +Backstage passes to a TAFKAL80ETC concert, 11, 24 +Backstage passes to a TAFKAL80ETC concert, 6, 28 +Backstage passes to a TAFKAL80ETC concert, 1, 32 +Backstage passes to a TAFKAL80ETC concert, -3, 0 +Backstage passes to a TAFKAL80ETC concert, -4, 0 + +-------- day 5 -------- +name, sellIn, quality +Normal Item, 5, 15 +Normal Item, -3, 0 +Normal Item, -5, 0 +Normal Item, -6, 0 +Aged Brie, 5, 25 +Aged Brie, -5, 40 +Aged Brie, 0, 50 +Sulfuras, Hand of Ragnaros, 10, 80 +Sulfuras, Hand of Ragnaros, -1, 80 +Backstage passes to a TAFKAL80ETC concert, 10, 25 +Backstage passes to a TAFKAL80ETC concert, 5, 30 +Backstage passes to a TAFKAL80ETC concert, 0, 35 +Backstage passes to a TAFKAL80ETC concert, -4, 0 +Backstage passes to a TAFKAL80ETC concert, -5, 0 + +-------- day 6 -------- +name, sellIn, quality +Normal Item, 4, 14 +Normal Item, -4, 0 +Normal Item, -6, 0 +Normal Item, -7, 0 +Aged Brie, 4, 26 +Aged Brie, -6, 42 +Aged Brie, -1, 50 +Sulfuras, Hand of Ragnaros, 10, 80 +Sulfuras, Hand of Ragnaros, -1, 80 +Backstage passes to a TAFKAL80ETC concert, 9, 27 +Backstage passes to a TAFKAL80ETC concert, 4, 33 +Backstage passes to a TAFKAL80ETC concert, -1, 0 +Backstage passes to a TAFKAL80ETC concert, -5, 0 +Backstage passes to a TAFKAL80ETC concert, -6, 0 + +-------- day 7 -------- +name, sellIn, quality +Normal Item, 3, 13 +Normal Item, -5, 0 +Normal Item, -7, 0 +Normal Item, -8, 0 +Aged Brie, 3, 27 +Aged Brie, -7, 44 +Aged Brie, -2, 50 +Sulfuras, Hand of Ragnaros, 10, 80 +Sulfuras, Hand of Ragnaros, -1, 80 +Backstage passes to a TAFKAL80ETC concert, 8, 29 +Backstage passes to a TAFKAL80ETC concert, 3, 36 +Backstage passes to a TAFKAL80ETC concert, -2, 0 +Backstage passes to a TAFKAL80ETC concert, -6, 0 +Backstage passes to a TAFKAL80ETC concert, -7, 0 + +-------- day 8 -------- +name, sellIn, quality +Normal Item, 2, 12 +Normal Item, -6, 0 +Normal Item, -8, 0 +Normal Item, -9, 0 +Aged Brie, 2, 28 +Aged Brie, -8, 46 +Aged Brie, -3, 50 +Sulfuras, Hand of Ragnaros, 10, 80 +Sulfuras, Hand of Ragnaros, -1, 80 +Backstage passes to a TAFKAL80ETC concert, 7, 31 +Backstage passes to a TAFKAL80ETC concert, 2, 39 +Backstage passes to a TAFKAL80ETC concert, -3, 0 +Backstage passes to a TAFKAL80ETC concert, -7, 0 +Backstage passes to a TAFKAL80ETC concert, -8, 0 + +-------- day 9 -------- +name, sellIn, quality +Normal Item, 1, 11 +Normal Item, -7, 0 +Normal Item, -9, 0 +Normal Item, -10, 0 +Aged Brie, 1, 29 +Aged Brie, -9, 48 +Aged Brie, -4, 50 +Sulfuras, Hand of Ragnaros, 10, 80 +Sulfuras, Hand of Ragnaros, -1, 80 +Backstage passes to a TAFKAL80ETC concert, 6, 33 +Backstage passes to a TAFKAL80ETC concert, 1, 42 +Backstage passes to a TAFKAL80ETC concert, -4, 0 +Backstage passes to a TAFKAL80ETC concert, -8, 0 +Backstage passes to a TAFKAL80ETC concert, -9, 0 + +-------- day 10 -------- +name, sellIn, quality +Normal Item, 0, 10 +Normal Item, -8, 0 +Normal Item, -10, 0 +Normal Item, -11, 0 +Aged Brie, 0, 30 +Aged Brie, -10, 50 +Aged Brie, -5, 50 +Sulfuras, Hand of Ragnaros, 10, 80 +Sulfuras, Hand of Ragnaros, -1, 80 +Backstage passes to a TAFKAL80ETC concert, 5, 35 +Backstage passes to a TAFKAL80ETC concert, 0, 45 +Backstage passes to a TAFKAL80ETC concert, -5, 0 +Backstage passes to a TAFKAL80ETC concert, -9, 0 +Backstage passes to a TAFKAL80ETC concert, -10, 0 + +-------- day 11 -------- +name, sellIn, quality +Normal Item, -1, 8 +Normal Item, -9, 0 +Normal Item, -11, 0 +Normal Item, -12, 0 +Aged Brie, -1, 32 +Aged Brie, -11, 50 +Aged Brie, -6, 50 +Sulfuras, Hand of Ragnaros, 10, 80 +Sulfuras, Hand of Ragnaros, -1, 80 +Backstage passes to a TAFKAL80ETC concert, 4, 38 +Backstage passes to a TAFKAL80ETC concert, -1, 0 +Backstage passes to a TAFKAL80ETC concert, -6, 0 +Backstage passes to a TAFKAL80ETC concert, -10, 0 +Backstage passes to a TAFKAL80ETC concert, -11, 0 + +-------- day 12 -------- +name, sellIn, quality +Normal Item, -2, 6 +Normal Item, -10, 0 +Normal Item, -12, 0 +Normal Item, -13, 0 +Aged Brie, -2, 34 +Aged Brie, -12, 50 +Aged Brie, -7, 50 +Sulfuras, Hand of Ragnaros, 10, 80 +Sulfuras, Hand of Ragnaros, -1, 80 +Backstage passes to a TAFKAL80ETC concert, 3, 41 +Backstage passes to a TAFKAL80ETC concert, -2, 0 +Backstage passes to a TAFKAL80ETC concert, -7, 0 +Backstage passes to a TAFKAL80ETC concert, -11, 0 +Backstage passes to a TAFKAL80ETC concert, -12, 0 + +-------- day 13 -------- +name, sellIn, quality +Normal Item, -3, 4 +Normal Item, -11, 0 +Normal Item, -13, 0 +Normal Item, -14, 0 +Aged Brie, -3, 36 +Aged Brie, -13, 50 +Aged Brie, -8, 50 +Sulfuras, Hand of Ragnaros, 10, 80 +Sulfuras, Hand of Ragnaros, -1, 80 +Backstage passes to a TAFKAL80ETC concert, 2, 44 +Backstage passes to a TAFKAL80ETC concert, -3, 0 +Backstage passes to a TAFKAL80ETC concert, -8, 0 +Backstage passes to a TAFKAL80ETC concert, -12, 0 +Backstage passes to a TAFKAL80ETC concert, -13, 0 + +-------- day 14 -------- +name, sellIn, quality +Normal Item, -4, 2 +Normal Item, -12, 0 +Normal Item, -14, 0 +Normal Item, -15, 0 +Aged Brie, -4, 38 +Aged Brie, -14, 50 +Aged Brie, -9, 50 +Sulfuras, Hand of Ragnaros, 10, 80 +Sulfuras, Hand of Ragnaros, -1, 80 +Backstage passes to a TAFKAL80ETC concert, 1, 47 +Backstage passes to a TAFKAL80ETC concert, -4, 0 +Backstage passes to a TAFKAL80ETC concert, -9, 0 +Backstage passes to a TAFKAL80ETC concert, -13, 0 +Backstage passes to a TAFKAL80ETC concert, -14, 0 + +-------- day 15 -------- +name, sellIn, quality +Normal Item, -5, 0 +Normal Item, -13, 0 +Normal Item, -15, 0 +Normal Item, -16, 0 +Aged Brie, -5, 40 +Aged Brie, -15, 50 +Aged Brie, -10, 50 +Sulfuras, Hand of Ragnaros, 10, 80 +Sulfuras, Hand of Ragnaros, -1, 80 +Backstage passes to a TAFKAL80ETC concert, 0, 50 +Backstage passes to a TAFKAL80ETC concert, -5, 0 +Backstage passes to a TAFKAL80ETC concert, -10, 0 +Backstage passes to a TAFKAL80ETC concert, -14, 0 +Backstage passes to a TAFKAL80ETC concert, -15, 0 + +-------- day 16 -------- +name, sellIn, quality +Normal Item, -6, 0 +Normal Item, -14, 0 +Normal Item, -16, 0 +Normal Item, -17, 0 +Aged Brie, -6, 42 +Aged Brie, -16, 50 +Aged Brie, -11, 50 +Sulfuras, Hand of Ragnaros, 10, 80 +Sulfuras, Hand of Ragnaros, -1, 80 +Backstage passes to a TAFKAL80ETC concert, -1, 0 +Backstage passes to a TAFKAL80ETC concert, -6, 0 +Backstage passes to a TAFKAL80ETC concert, -11, 0 +Backstage passes to a TAFKAL80ETC concert, -15, 0 +Backstage passes to a TAFKAL80ETC concert, -16, 0 + +-------- day 17 -------- +name, sellIn, quality +Normal Item, -7, 0 +Normal Item, -15, 0 +Normal Item, -17, 0 +Normal Item, -18, 0 +Aged Brie, -7, 44 +Aged Brie, -17, 50 +Aged Brie, -12, 50 +Sulfuras, Hand of Ragnaros, 10, 80 +Sulfuras, Hand of Ragnaros, -1, 80 +Backstage passes to a TAFKAL80ETC concert, -2, 0 +Backstage passes to a TAFKAL80ETC concert, -7, 0 +Backstage passes to a TAFKAL80ETC concert, -12, 0 +Backstage passes to a TAFKAL80ETC concert, -16, 0 +Backstage passes to a TAFKAL80ETC concert, -17, 0 + +-------- day 18 -------- +name, sellIn, quality +Normal Item, -8, 0 +Normal Item, -16, 0 +Normal Item, -18, 0 +Normal Item, -19, 0 +Aged Brie, -8, 46 +Aged Brie, -18, 50 +Aged Brie, -13, 50 +Sulfuras, Hand of Ragnaros, 10, 80 +Sulfuras, Hand of Ragnaros, -1, 80 +Backstage passes to a TAFKAL80ETC concert, -3, 0 +Backstage passes to a TAFKAL80ETC concert, -8, 0 +Backstage passes to a TAFKAL80ETC concert, -13, 0 +Backstage passes to a TAFKAL80ETC concert, -17, 0 +Backstage passes to a TAFKAL80ETC concert, -18, 0 + +-------- day 19 -------- +name, sellIn, quality +Normal Item, -9, 0 +Normal Item, -17, 0 +Normal Item, -19, 0 +Normal Item, -20, 0 +Aged Brie, -9, 48 +Aged Brie, -19, 50 +Aged Brie, -14, 50 +Sulfuras, Hand of Ragnaros, 10, 80 +Sulfuras, Hand of Ragnaros, -1, 80 +Backstage passes to a TAFKAL80ETC concert, -4, 0 +Backstage passes to a TAFKAL80ETC concert, -9, 0 +Backstage passes to a TAFKAL80ETC concert, -14, 0 +Backstage passes to a TAFKAL80ETC concert, -18, 0 +Backstage passes to a TAFKAL80ETC concert, -19, 0 + +-------- day 20 -------- +name, sellIn, quality +Normal Item, -10, 0 +Normal Item, -18, 0 +Normal Item, -20, 0 +Normal Item, -21, 0 +Aged Brie, -10, 50 +Aged Brie, -20, 50 +Aged Brie, -15, 50 +Sulfuras, Hand of Ragnaros, 10, 80 +Sulfuras, Hand of Ragnaros, -1, 80 +Backstage passes to a TAFKAL80ETC concert, -5, 0 +Backstage passes to a TAFKAL80ETC concert, -10, 0 +Backstage passes to a TAFKAL80ETC concert, -15, 0 +Backstage passes to a TAFKAL80ETC concert, -19, 0 +Backstage passes to a TAFKAL80ETC concert, -20, 0 + +-------- day 21 -------- +name, sellIn, quality +Normal Item, -11, 0 +Normal Item, -19, 0 +Normal Item, -21, 0 +Normal Item, -22, 0 +Aged Brie, -11, 50 +Aged Brie, -21, 50 +Aged Brie, -16, 50 +Sulfuras, Hand of Ragnaros, 10, 80 +Sulfuras, Hand of Ragnaros, -1, 80 +Backstage passes to a TAFKAL80ETC concert, -6, 0 +Backstage passes to a TAFKAL80ETC concert, -11, 0 +Backstage passes to a TAFKAL80ETC concert, -16, 0 +Backstage passes to a TAFKAL80ETC concert, -20, 0 +Backstage passes to a TAFKAL80ETC concert, -21, 0 + +-------- day 22 -------- +name, sellIn, quality +Normal Item, -12, 0 +Normal Item, -20, 0 +Normal Item, -22, 0 +Normal Item, -23, 0 +Aged Brie, -12, 50 +Aged Brie, -22, 50 +Aged Brie, -17, 50 +Sulfuras, Hand of Ragnaros, 10, 80 +Sulfuras, Hand of Ragnaros, -1, 80 +Backstage passes to a TAFKAL80ETC concert, -7, 0 +Backstage passes to a TAFKAL80ETC concert, -12, 0 +Backstage passes to a TAFKAL80ETC concert, -17, 0 +Backstage passes to a TAFKAL80ETC concert, -21, 0 +Backstage passes to a TAFKAL80ETC concert, -22, 0 + +-------- day 23 -------- +name, sellIn, quality +Normal Item, -13, 0 +Normal Item, -21, 0 +Normal Item, -23, 0 +Normal Item, -24, 0 +Aged Brie, -13, 50 +Aged Brie, -23, 50 +Aged Brie, -18, 50 +Sulfuras, Hand of Ragnaros, 10, 80 +Sulfuras, Hand of Ragnaros, -1, 80 +Backstage passes to a TAFKAL80ETC concert, -8, 0 +Backstage passes to a TAFKAL80ETC concert, -13, 0 +Backstage passes to a TAFKAL80ETC concert, -18, 0 +Backstage passes to a TAFKAL80ETC concert, -22, 0 +Backstage passes to a TAFKAL80ETC concert, -23, 0 + +-------- day 24 -------- +name, sellIn, quality +Normal Item, -14, 0 +Normal Item, -22, 0 +Normal Item, -24, 0 +Normal Item, -25, 0 +Aged Brie, -14, 50 +Aged Brie, -24, 50 +Aged Brie, -19, 50 +Sulfuras, Hand of Ragnaros, 10, 80 +Sulfuras, Hand of Ragnaros, -1, 80 +Backstage passes to a TAFKAL80ETC concert, -9, 0 +Backstage passes to a TAFKAL80ETC concert, -14, 0 +Backstage passes to a TAFKAL80ETC concert, -19, 0 +Backstage passes to a TAFKAL80ETC concert, -23, 0 +Backstage passes to a TAFKAL80ETC concert, -24, 0 + +-------- day 25 -------- +name, sellIn, quality +Normal Item, -15, 0 +Normal Item, -23, 0 +Normal Item, -25, 0 +Normal Item, -26, 0 +Aged Brie, -15, 50 +Aged Brie, -25, 50 +Aged Brie, -20, 50 +Sulfuras, Hand of Ragnaros, 10, 80 +Sulfuras, Hand of Ragnaros, -1, 80 +Backstage passes to a TAFKAL80ETC concert, -10, 0 +Backstage passes to a TAFKAL80ETC concert, -15, 0 +Backstage passes to a TAFKAL80ETC concert, -20, 0 +Backstage passes to a TAFKAL80ETC concert, -24, 0 +Backstage passes to a TAFKAL80ETC concert, -25, 0 + +-------- day 26 -------- +name, sellIn, quality +Normal Item, -16, 0 +Normal Item, -24, 0 +Normal Item, -26, 0 +Normal Item, -27, 0 +Aged Brie, -16, 50 +Aged Brie, -26, 50 +Aged Brie, -21, 50 +Sulfuras, Hand of Ragnaros, 10, 80 +Sulfuras, Hand of Ragnaros, -1, 80 +Backstage passes to a TAFKAL80ETC concert, -11, 0 +Backstage passes to a TAFKAL80ETC concert, -16, 0 +Backstage passes to a TAFKAL80ETC concert, -21, 0 +Backstage passes to a TAFKAL80ETC concert, -25, 0 +Backstage passes to a TAFKAL80ETC concert, -26, 0 + +-------- day 27 -------- +name, sellIn, quality +Normal Item, -17, 0 +Normal Item, -25, 0 +Normal Item, -27, 0 +Normal Item, -28, 0 +Aged Brie, -17, 50 +Aged Brie, -27, 50 +Aged Brie, -22, 50 +Sulfuras, Hand of Ragnaros, 10, 80 +Sulfuras, Hand of Ragnaros, -1, 80 +Backstage passes to a TAFKAL80ETC concert, -12, 0 +Backstage passes to a TAFKAL80ETC concert, -17, 0 +Backstage passes to a TAFKAL80ETC concert, -22, 0 +Backstage passes to a TAFKAL80ETC concert, -26, 0 +Backstage passes to a TAFKAL80ETC concert, -27, 0 + +-------- day 28 -------- +name, sellIn, quality +Normal Item, -18, 0 +Normal Item, -26, 0 +Normal Item, -28, 0 +Normal Item, -29, 0 +Aged Brie, -18, 50 +Aged Brie, -28, 50 +Aged Brie, -23, 50 +Sulfuras, Hand of Ragnaros, 10, 80 +Sulfuras, Hand of Ragnaros, -1, 80 +Backstage passes to a TAFKAL80ETC concert, -13, 0 +Backstage passes to a TAFKAL80ETC concert, -18, 0 +Backstage passes to a TAFKAL80ETC concert, -23, 0 +Backstage passes to a TAFKAL80ETC concert, -27, 0 +Backstage passes to a TAFKAL80ETC concert, -28, 0 + +-------- day 29 -------- +name, sellIn, quality +Normal Item, -19, 0 +Normal Item, -27, 0 +Normal Item, -29, 0 +Normal Item, -30, 0 +Aged Brie, -19, 50 +Aged Brie, -29, 50 +Aged Brie, -24, 50 +Sulfuras, Hand of Ragnaros, 10, 80 +Sulfuras, Hand of Ragnaros, -1, 80 +Backstage passes to a TAFKAL80ETC concert, -14, 0 +Backstage passes to a TAFKAL80ETC concert, -19, 0 +Backstage passes to a TAFKAL80ETC concert, -24, 0 +Backstage passes to a TAFKAL80ETC concert, -28, 0 +Backstage passes to a TAFKAL80ETC concert, -29, 0 + +-------- day 30 -------- +name, sellIn, quality +Normal Item, -20, 0 +Normal Item, -28, 0 +Normal Item, -30, 0 +Normal Item, -31, 0 +Aged Brie, -20, 50 +Aged Brie, -30, 50 +Aged Brie, -25, 50 +Sulfuras, Hand of Ragnaros, 10, 80 +Sulfuras, Hand of Ragnaros, -1, 80 +Backstage passes to a TAFKAL80ETC concert, -15, 0 +Backstage passes to a TAFKAL80ETC concert, -20, 0 +Backstage passes to a TAFKAL80ETC concert, -25, 0 +Backstage passes to a TAFKAL80ETC concert, -29, 0 +Backstage passes to a TAFKAL80ETC concert, -30, 0 + +'''; +} From 56553265430d52c189fcab091d8a98860b26e9e4 Mon Sep 17 00:00:00 2001 From: fiatcode Date: Wed, 18 Feb 2026 12:35:02 +0700 Subject: [PATCH 2/7] REFACTOR: extract item type methods from nested conditionals --- lib/gilded_rose.dart | 112 +++++++++++++++++++++++++------------------ 1 file changed, 65 insertions(+), 47 deletions(-) diff --git a/lib/gilded_rose.dart b/lib/gilded_rose.dart index 5c87d3a..2322c6e 100644 --- a/lib/gilded_rose.dart +++ b/lib/gilded_rose.dart @@ -17,55 +17,73 @@ class GildedRose { GildedRose(this.items); void updateQuality() { - for (var i = 0; i < items.length; i++) { - if (items[i].name != 'Aged Brie' && - items[i].name != 'Backstage passes to a TAFKAL80ETC concert') { - if (items[i].quality > 0) { - if (items[i].name != 'Sulfuras, Hand of Ragnaros') { - items[i].quality = items[i].quality - 1; - } - } + for (final item in items) { + if (item.name == 'Sulfuras, Hand of Ragnaros') { + _updateSulfuras(item); + } else if (item.name == 'Aged Brie') { + _updateAgedBrie(item); + } else if (item.name == 'Backstage passes to a TAFKAL80ETC concert') { + _updateBackstagePasses(item); } else { - if (items[i].quality < 50) { - items[i].quality = items[i].quality + 1; - - if (items[i].name == 'Backstage passes to a TAFKAL80ETC concert') { - if (items[i].sellIn < 11) { - if (items[i].quality < 50) { - items[i].quality = items[i].quality + 1; - } - } - - if (items[i].sellIn < 6) { - if (items[i].quality < 50) { - items[i].quality = items[i].quality + 1; - } - } - } - } - } - - if (items[i].name != 'Sulfuras, Hand of Ragnaros') { - items[i].sellIn = items[i].sellIn - 1; - } - - if (items[i].sellIn < 0) { - if (items[i].name != 'Aged Brie') { - if (items[i].name != 'Backstage passes to a TAFKAL80ETC concert') { - if (items[i].quality > 0) { - if (items[i].name != 'Sulfuras, Hand of Ragnaros') { - items[i].quality = items[i].quality - 1; - } - } - } else { - items[i].quality = items[i].quality - items[i].quality; - } - } else { - if (items[i].quality < 50) { - items[i].quality = items[i].quality + 1; - } - } + _updateNormalItem(item); } } } + + void _updateNormalItem(Item item) { + // Quality decreases by 1 each day + if (item.quality > 0) { + item.quality -= 1; + } + + item.sellIn -= 1; + + // After sell-by date, quality degrades twice as fast + if (item.sellIn < 0 && item.quality > 0) { + item.quality -= 1; + } + } + + void _updateAgedBrie(Item item) { + // Quality increases as it ages + if (item.quality < 50) { + item.quality += 1; + } + + item.sellIn -= 1; + + // After sell-by date, quality increases twice as fast + if (item.sellIn < 0 && item.quality < 50) { + item.quality += 1; + } + } + + void _updateBackstagePasses(Item item) { + // Quality increases as concert approaches + if (item.quality < 50) { + item.quality += 1; + + // 10 days or less: +2 quality/day + if (item.sellIn < 11 && item.quality < 50) { + item.quality += 1; + } + + // 5 days or less: +3 quality/day + if (item.sellIn < 6 && item.quality < 50) { + item.quality += 1; + } + } + + item.sellIn -= 1; + + // After concert, quality drops to 0 + if (item.sellIn < 0) { + item.quality = 0; + } + } + + void _updateSulfuras(Item item) { + // Legendary item never changes + // Quality always 80, sellIn never decreases + } } From 7a1779091577729b4da1fe5b44519bae88c9a7fe Mon Sep 17 00:00:00 2001 From: fiatcode Date: Wed, 18 Feb 2026 12:37:13 +0700 Subject: [PATCH 3/7] REFACTOR: introduce strategy pattern for item types --- lib/gilded_rose.dart | 66 +++++++++++++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 22 deletions(-) diff --git a/lib/gilded_rose.dart b/lib/gilded_rose.dart index 2322c6e..413c60f 100644 --- a/lib/gilded_rose.dart +++ b/lib/gilded_rose.dart @@ -11,26 +11,14 @@ class Item { String toString() => '$name, $sellIn, $quality'; } -class GildedRose { - List items; +/// Strategy pattern: Each item type has its own update behavior +abstract class ItemUpdater { + void update(Item item); +} - GildedRose(this.items); - - void updateQuality() { - for (final item in items) { - if (item.name == 'Sulfuras, Hand of Ragnaros') { - _updateSulfuras(item); - } else if (item.name == 'Aged Brie') { - _updateAgedBrie(item); - } else if (item.name == 'Backstage passes to a TAFKAL80ETC concert') { - _updateBackstagePasses(item); - } else { - _updateNormalItem(item); - } - } - } - - void _updateNormalItem(Item item) { +class NormalItemUpdater implements ItemUpdater { + @override + void update(Item item) { // Quality decreases by 1 each day if (item.quality > 0) { item.quality -= 1; @@ -43,8 +31,11 @@ class GildedRose { item.quality -= 1; } } +} - void _updateAgedBrie(Item item) { +class AgedBrieUpdater implements ItemUpdater { + @override + void update(Item item) { // Quality increases as it ages if (item.quality < 50) { item.quality += 1; @@ -57,8 +48,11 @@ class GildedRose { item.quality += 1; } } +} - void _updateBackstagePasses(Item item) { +class BackstagePassUpdater implements ItemUpdater { + @override + void update(Item item) { // Quality increases as concert approaches if (item.quality < 50) { item.quality += 1; @@ -81,9 +75,37 @@ class GildedRose { item.quality = 0; } } +} - void _updateSulfuras(Item item) { +class SulfurasUpdater implements ItemUpdater { + @override + void update(Item item) { // Legendary item never changes // Quality always 80, sellIn never decreases } } + +class GildedRose { + List items; + + GildedRose(this.items); + + void updateQuality() { + for (final item in items) { + final updater = _selectUpdater(item); + updater.update(item); + } + } + + ItemUpdater _selectUpdater(Item item) { + if (item.name == 'Sulfuras, Hand of Ragnaros') { + return SulfurasUpdater(); + } else if (item.name == 'Aged Brie') { + return AgedBrieUpdater(); + } else if (item.name == 'Backstage passes to a TAFKAL80ETC concert') { + return BackstagePassUpdater(); + } else { + return NormalItemUpdater(); + } + } +} From c2994ce63c27e73bfcfc72ce3bbac4abcb7da70c Mon Sep 17 00:00:00 2001 From: fiatcode Date: Wed, 18 Feb 2026 12:39:31 +0700 Subject: [PATCH 4/7] REFACTOR: extract helper methods and domain constants --- lib/gilded_rose.dart | 52 +++++++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/lib/gilded_rose.dart b/lib/gilded_rose.dart index 413c60f..604c119 100644 --- a/lib/gilded_rose.dart +++ b/lib/gilded_rose.dart @@ -11,6 +11,20 @@ class Item { String toString() => '$name, $sellIn, $quality'; } +/// 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) { + item.quality = (item.quality - amount).clamp(_minQuality, _maxQuality); +} + +void _improveQuality(Item item, int amount) { + item.quality = (item.quality + amount).clamp(_minQuality, _maxQuality); +} + /// Strategy pattern: Each item type has its own update behavior abstract class ItemUpdater { void update(Item item); @@ -20,15 +34,13 @@ class NormalItemUpdater implements ItemUpdater { @override void update(Item item) { // Quality decreases by 1 each day - if (item.quality > 0) { - item.quality -= 1; - } + _degradeQuality(item, 1); item.sellIn -= 1; // After sell-by date, quality degrades twice as fast - if (item.sellIn < 0 && item.quality > 0) { - item.quality -= 1; + if (item.sellIn < 0) { + _degradeQuality(item, 1); } } } @@ -37,15 +49,13 @@ class AgedBrieUpdater implements ItemUpdater { @override void update(Item item) { // Quality increases as it ages - if (item.quality < 50) { - item.quality += 1; - } + _improveQuality(item, 1); item.sellIn -= 1; // After sell-by date, quality increases twice as fast - if (item.sellIn < 0 && item.quality < 50) { - item.quality += 1; + if (item.sellIn < 0) { + _improveQuality(item, 1); } } } @@ -53,26 +63,24 @@ class AgedBrieUpdater implements ItemUpdater { class BackstagePassUpdater implements ItemUpdater { @override void update(Item item) { - // Quality increases as concert approaches - if (item.quality < 50) { - item.quality += 1; + // Base quality increase + _improveQuality(item, 1); - // 10 days or less: +2 quality/day - if (item.sellIn < 11 && item.quality < 50) { - item.quality += 1; - } + // 10 days or less: +1 additional + if (item.sellIn <= 10) { + _improveQuality(item, 1); + } - // 5 days or less: +3 quality/day - if (item.sellIn < 6 && item.quality < 50) { - item.quality += 1; - } + // 5 days or less: +1 additional (total +3/day) + if (item.sellIn <= 5) { + _improveQuality(item, 1); } item.sellIn -= 1; // After concert, quality drops to 0 if (item.sellIn < 0) { - item.quality = 0; + item.quality = _minQuality; } } } From 13f6bc231fe12e89ee71f63726e81986370f7108 Mon Sep 17 00:00:00 2001 From: fiatcode Date: Wed, 18 Feb 2026 12:41:33 +0700 Subject: [PATCH 5/7] RED: add tests for conjured items --- test/gilded_rose_test.dart | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/gilded_rose_test.dart b/test/gilded_rose_test.dart index fbe0ed9..f79d761 100644 --- a/test/gilded_rose_test.dart +++ b/test/gilded_rose_test.dart @@ -143,6 +143,38 @@ void main() { }); }); + group('Conjured items', () { + test('degrade in quality twice as fast as normal items', () { + final items = [Item('Conjured Mana Cake', 10, 20)]; + final gildedRose = GildedRose(items); + + gildedRose.updateQuality(); + + expect(items[0].quality, equals(18)); // -2 instead of -1 + expect(items[0].sellIn, equals(9)); + }); + + test('degrade twice as fast after sell-by date', () { + final items = [Item('Conjured Mana Cake', 0, 20)]; + final gildedRose = GildedRose(items); + + gildedRose.updateQuality(); + + expect(items[0].quality, equals(16)); // -4 instead of -2 + expect(items[0].sellIn, equals(-1)); + }); + + test('never have negative quality', () { + final items = [Item('Conjured Mana Cake', 5, 1)]; + final gildedRose = GildedRose(items); + + gildedRose.updateQuality(); + + expect(items[0].quality, equals(0)); + expect(items[0].sellIn, equals(4)); + }); + }); + group('Golden Master - 30-day simulation', () { test('captures complete behavior of all item types over time', () { final items = _createAllItemTypes(); From 4d436517e7862bca8a5d186d46815ed0e5dc0c47 Mon Sep 17 00:00:00 2001 From: fiatcode Date: Wed, 18 Feb 2026 12:42:42 +0700 Subject: [PATCH 6/7] GREEN: implement conjured items degrading twice as fast --- lib/gilded_rose.dart | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/gilded_rose.dart b/lib/gilded_rose.dart index 604c119..01c7722 100644 --- a/lib/gilded_rose.dart +++ b/lib/gilded_rose.dart @@ -93,6 +93,21 @@ class SulfurasUpdater implements ItemUpdater { } } +class ConjuredItemUpdater implements ItemUpdater { + @override + void update(Item item) { + // Conjured items degrade twice as fast as normal items + _degradeQuality(item, 2); + + item.sellIn -= 1; + + // After sell-by date, quality degrades twice as fast (4x total) + if (item.sellIn < 0) { + _degradeQuality(item, 2); + } + } +} + class GildedRose { List items; @@ -112,6 +127,8 @@ class GildedRose { return AgedBrieUpdater(); } else if (item.name == 'Backstage passes to a TAFKAL80ETC concert') { return BackstagePassUpdater(); + } else if (item.name.startsWith('Conjured')) { + return ConjuredItemUpdater(); } else { return NormalItemUpdater(); } From d602d302eef744b69c4335947e465e69dbd635f5 Mon Sep 17 00:00:00 2001 From: fiatcode Date: Wed, 18 Feb 2026 12:46:12 +0700 Subject: [PATCH 7/7] docs: document gilded rose kata --- README.md | 334 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 317 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index a975be9..e7c506b 100644 --- a/README.md +++ b/README.md @@ -102,13 +102,120 @@ Tests for simple cases (gutter game, one spare, one strike) drove an algorithm t --- -### 3. [Third Kata Name] 🚧 +### 3. Gilded Rose ✅ -**Status:** Not yet started -**Implementation:** `lib/[third_kata].dart` -**Tests:** `test/[third_kata]_test.dart` +**Implementation:** [`lib/gilded_rose.dart`](lib/gilded_rose.dart) +**Tests:** [`test/gilded_rose_test.dart`](test/gilded_rose_test.dart) -*Coming soon...* +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:** + +1. **Normal Items:** Quality decreases by 1/day, 2/day after sell-by date +2. **Aged Brie:** Quality increases by 1/day, 2/day after expiration (improves with age!) +3. **Sulfuras (Legendary):** Never changes, quality always 80, never "expires" +4. **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 +5. **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 `Item` class (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** +```dart +// 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):** +```dart +// Before: 60 lines of nested hell +// After: Clean if-else delegating to 4 private methods +_updateNormalItem(), _updateAgedBrie(), +_updateBackstagePasses(), _updateSulfuras() +``` + +**Step 2 - Strategy Pattern (REFACTOR):** +```dart +abstract class ItemUpdater { + void update(Item item); +} + +class NormalItemUpdater implements ItemUpdater { ... } +class AgedBrieUpdater implements ItemUpdater { ... } +// ... 4 concrete strategies +``` + +**Step 3 - Domain Helpers (REFACTOR):** +```dart +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 + +```dart +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 + +1. **Characterization Tests = Freedom:** With tests in place, aggressive refactoring felt safe. Every change validated instantly. + +2. **Strategy Pattern = Extensibility:** Adding Conjured items took **5 minutes**. Before refactoring, it would have meant diving into nested conditionals and risking bugs. + +3. **Small Steps = Big Wins:** Three refactoring commits transformed spaghetti into clean code. Each step kept tests GREEN, proving behavior preservation. + +4. **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." --- @@ -121,6 +228,7 @@ 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 @@ -244,19 +352,203 @@ Tests are organized by **scoring complexity**, mirroring how the domain rules bu --- +## 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: +1. Understand what it does (without breaking it) +2. Add tests to capture behavior +3. Refactor safely +4. 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:** +```dart +// 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:** +```dart +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:** +```dart +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 +```dart +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 +```dart +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 -### Roman Numerals vs. Bowling Game +### All Three Katas at a Glance -| 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 | +| 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 @@ -269,14 +561,21 @@ Tests are organized by **scoring complexity**, mirroring how the domain rules bu - 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 1. **Roman Numerals first:** Learn TDD fundamentals without state complexity 2. **Bowling Game second:** Apply TDD to stateful problems -3. **Next kata:** Choose based on what you want to practice: +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 - - **Gilded Rose:** Refactoring legacy code without tests + - **Mars Rover:** Command pattern, multiple behaviors + - **Prime Factors:** Mathematical decomposition, algorithmic thinkingsts - **Mars Rover:** Command pattern, multiple behaviors --- @@ -284,6 +583,7 @@ Tests are organized by **scoring complexity**, mirroring how the domain rules bu ## References ### General TDD & Clean Code +- [Gilded Rose Kata](https://github.com/emilybache/GildedRose-Refactoring-Kata) - [Clean Code by Robert C. Martin](https://www.oreilly.com/library/view/clean-code-a/9780136083238/) - [Domain-Driven Design by Eric Evans](https://www.domainlanguage.com/ddd/) - [Test-Driven Development by Kent Beck](https://www.amazon.com/Test-Driven-Development-Kent-Beck/dp/0321146530)