I have done the supermarket pricing kata in TDD style and I would appreciate it if someone could review it for me.
Kata:
"...checkout that calculates the total price of a number of items. In a normal supermarket, things are identified using Stock Keeping Units, or SKUs. In our store, we’ll use individual letters of the alphabet (A, B, C, and so on). Our goods are priced individually. In addition, some items are multipriced: buy n of them, and they’ll cost you y cents. For example, item ‘A’ might cost 50 cents individually, but this week we have a special offer: buy three ‘A’s and they’ll cost you $1.30. In fact this week’s prices are:"
| Item Name | Price | Special Price | |:-----------|------------:|:------------: | | A | 50 | 3 for 130 | | B | 30 | 2 for 45 |
"Our checkout accepts items in any order, so that if we scan a B, an A, and another B, we’ll recognize the two B’s and price them at 45 (for a total price so far of 95). Because the pricing changes frequently, we need to be able to pass in a set of pricing rules each time we start handling a checkout transaction. "
Tests:
public class CheckoutTests
{
private Checkout checkout;
private readonly Item itemA = new Item
{
Name = "A",
Price = 50
};
private readonly Item itemB = new Item
{
Name = "B",
Price = 30
};
private readonly PricingRule ItemARule = new PricingRule
{
ItemName = "A",
ItemCount = 3,
Total = 130
};
private readonly PricingRule ItemBRule = new PricingRule
{
ItemName = "B",
ItemCount = 2,
Total = 45
};
[SetUp]
public void SetUp()
{
checkout = new Checkout();
}
private void AddItems(int count,
Item item)
{
for (int i = 0;
i < count;
i++)
{
checkout.AddItem(item);
}
}
[Test]
public void TestCheckoutZeroItemReturnsZero()
{
int result = checkout.CalculateTotal();
Assert.AreEqual(0,
result);
}
[Test]
public void TestCheckoutOneItemsPriceAsTotal()
{
AddItems(1,
itemA);
int result = checkout.CalculateTotal();
Assert.AreEqual(50,
result);
}
[Test]
[ExpectedException(typeof (ArgumentException))]
public void TestAddingPricingRuleWithoutNameThrowsException()
{
checkout.AddPricingRule(new PricingRule
{
ItemCount = 2,
Total = 45
});
}
[Test]
public void TestCheckoutWithSpecialPriceCriteriaMetReturnsSpecialPrice()
{
checkout.AddPricingRule(ItemARule);
AddItems(3,
itemA);
int result = checkout.CalculateTotal();
Assert.AreEqual(130,
result);
}
[Test]
public void TestCheckoutWithSpecialPriceCriteriaNotMetReturnsTotal()
{
checkout.AddPricingRule(ItemARule);
AddItems(2,
itemA);
int result = checkout.CalculateTotal();
Assert.AreEqual(100,
result);
}
[Test]
public void TestCheckoutWithSpecialPriceCriteriaMetAndExtraItemsReturnsSpecialPricePlusExtraItemsTotal()
{
checkout.AddPricingRule(ItemARule);
AddItems(4,
itemA);
int result = checkout.CalculateTotal();
Assert.AreEqual(180,
result);
}
[Test]
public void TestCheckoutWithMultipleGroupsOfItemsWithSpecialPriceCriteriaMet()
{
checkout.AddPricingRule(ItemARule);
checkout.AddPricingRule(ItemBRule);
AddItems(4,
itemA);
AddItems(3,
itemB);
int result = checkout.CalculateTotal();
Assert.AreEqual(255,
result);
}
}
Implementation:
public class Checkout
{
private readonly List<Item> items = new List<Item>();
private readonly List<PricingRule> pricingRules = new List<PricingRule>();
public int CalculateTotal()
{
int total = 0;
var itemGroups = items.GroupBy(g => g.Name);
foreach (var itemGroup in itemGroups)
{
var ruleForGroup = pricingRules.FirstOrDefault(r => r.ItemName == itemGroup.Key);
if (ruleForGroup != null)
{
var groupCount = itemGroup.Count();
var extra = groupCount - ruleForGroup.ItemCount;
if (extra < 0)
{
total += itemGroup.Sum(g => g.Price);
}
else
{
total += ruleForGroup.Total;
total += extra*itemGroup.First()
.Price;
}
}
else
{
total += itemGroup.Sum(x => x.Price);
}
}
return total;
}
public void AddItem(Item item)
{
items.Add(item);
}
public void AddPricingRule(PricingRule rule)
{
if (rule == null)
{
throw new ArgumentNullException();
}
if (string.IsNullOrWhiteSpace(rule.ItemName))
{
throw new ArgumentException();
}
pricingRules.Add(rule);
}
}