Checkout

tdd code checkout

This kata comes from here.

I realized doing this post after the code was written is quite difficult, as a lot of the juicy details stuff got refactored away. So please ask me if you don’t understand any section!

Premise:

Given a bunch of items, their price per item, and a special bulk discount rate: create a system that can add items to your basket and a checkout system that can scan your basket and output the final price, taking the bulk discount rate into consideration. (i.e. item A costs $50, but you can buy 3 for $130)

Got it? Good. For this Kata I used C# with NUnit, developed in Visual Studio 2015.

Process

I started with my failing unit test with something like this:

CheckoutCounter.Scan(itemName);
Assert.That(CheckoutCounter.CalculateTotalPrice(), Is.EqualTo(expectedPrice).Within(0.0001));
  • There is some CheckoutCounter class that can scan items by name
  • The CheckoutCounter can calculate the total price

From this I realize that I will probably need some sort of data structure for cataloging the item names and their prices. I used a Dictionary\<string, double>:

var itemPrices = new Dictionary<string, double>
{
    {"A", 5.0 },
    {"B", 10.0 },
};

The first, very degenerate assertion is testing for item with no name, which should return zero:

CheckoutCounter.Scan("");
Assert.That(CheckoutCounter.CalculateTotalPrice(), Is.EqualTo(0).Within(0.0001));

Making this test case pass is easy. I create the skeleton for CheckoutCounter class, and simply have CalculateTotalPrice() return 0. I’ll skip the code here. Next I refactor this test case to something more readable, then added the next assertion:

[Test]
public void ScanningSingleItems()
{
    var itemPrices = new Dictionary<string, double>
    {
        {"A", 5.0 },
        {"B", 10.0 },
    };
    CheckoutCounter.SetItemPrices(itemPrices);
    CheckoutCounter.Reset();
    AssertCheckoutTotal("", 0.0f);
    AssertCheckoutTotal("A", 5.0f);

}

private static void AssertCheckoutTotal(string itemName, float expected)
{
    CheckoutCounter.Scan(itemName);
    Assert.That(CheckoutCounter.CalculateTotalPrice(), Is.EqualTo(expected).Within(0.0001));
}

Now I can’t just have CalculateTotalPrice() return 0 anymore, so I have to somewhat smarter with my implementation. My thought process went like this:

  • Everytime I can scan, I can simply using a private “total” variable and add the scanned price to itself
  • This worked for next two cases
...
AssertCheckoutTotal("A", 10.0f);
AssertCheckoutTotal("B", 20.0f);

At this point there’s no point adding more, as my tests showed that simply adding items works. Now next test case is more interesting. I added discounts. This was my complete test for scanning single items:

[Test]
public void ScanningSingleItems()
{
    var itemPrices = new Dictionary<string, double>
    {
        {"A", 5.0 },
        {"B", 10.0 },
    };
    var discounts = new Dictionary<string, Discount>()
    {
        { "A", new Discount(3, 12.0f) }
    };
    Discounts.SetDiscounts(discounts);
    CheckoutCounter.SetItemPrices(itemPrices);
    CheckoutCounter.Reset();
    AssertCheckoutTotal("", 0.0f);
    AssertCheckoutTotal("A", 5.0f);
    AssertCheckoutTotal("A", 10.0f);
    AssertCheckoutTotal("B", 20.0f);
    AssertCheckoutTotal("A", 22.0f);
    AssertCheckoutTotal("A", 27.0f);
}

Discounts is a static class that contains a list a of Discount value objects. Each Discount object specifies how much for how many: public struct Discount

{
    public int HowMany;
    public double ForHowMuch;

    public Discount(int howMany, double forHowMuch)
    {
        this.HowMany = howMany;
        this.ForHowMuch = forHowMuch;
    }

    public double DiscountedItemPrice()
    {
        return ForHowMuch/HowMany;
    }
}

The CheckoutCounter processes the Discount when calculating the total price by passing in the checkout quantity, and subtract the discount from the non-discounted price. The code is pretty much working at this point, here’s the complete CheckoutCounter class:

public class CheckoutCounter
{
    private static Dictionary<string, double> itemPrices = new Dictionary<string, double>();
    private static Dictionary<string, int> itemQuantities = new Dictionary<string, int>();

    public static void Scan(string itemName)
    {
        if (itemName.Length != 0)
        {
            if (itemPrices.ContainsKey(itemName))
                AddItem(itemName);
            else
                throw new ItemNotFoundException();
        }
    }

    public static double CalculateTotalPrice()
    {
        return itemQuantities.Sum(item => item.Value*itemPrices[item.Key] - GetItemDiscount(item.Key));
    }

    private static double GetItemDiscount(string itemName)
    {

        return Discounts.GetDiscount(itemName, itemQuantities[itemName], itemPrices[itemName]);
    }

    private static void AddItem(string itemName)
    {
        int quantity;
        if (itemQuantities.TryGetValue(itemName, out quantity))
            itemQuantities[itemName] = ++quantity;
        else
            itemQuantities.Add(itemName, quantity = 1);
    }

    public static void SetItemPrices(Dictionary<string, double> newItemPrices)
    {
        itemPrices = newItemPrices;
    }

    public static void Reset()
    {
        itemQuantities.Clear();
    }
}

The code should be self explanatory. It is very straight forward:

  • On Scan() – If the the item exists and has a price, add the item
  • On AddItem() – increment the item quantity keyed by the item name
  • On CalculateTotalPrice() – Sum over every item in the quantities dictionary, and subtract the corresponding discount based on the item quantity
  • Rest are simple things like setters and resets Finally for my acceptance test (partially copied from the kata website):
[Test]
public void ContiniousScanning()
{
    var itemPrices = new Dictionary<string, double>
    {
        {"A", 50.0 },
        {"B", 30.0 },
        {"C", 20.0 },
        {"D", 15.0 },
    };
    var discounts = new Dictionary<string, Discount>()
    {
        { "A", new Discount(3, 130.0f) },
        { "B", new Discount(2, 45.0f) },
    };
    Discounts.SetDiscounts(discounts);
    CheckoutCounter.SetItemPrices(itemPrices);

    AssertSequenceCheckoutTotal("", 0);
    AssertSequenceCheckoutTotal("A", 50);
    AssertSequenceCheckoutTotal("AB", 80);
    AssertSequenceCheckoutTotal("CDBA", 115);
    AssertSequenceCheckoutTotal("AA", 100);
    AssertSequenceCheckoutTotal("AAA", 130);
    AssertSequenceCheckoutTotal("AAAA", 180);
    AssertSequenceCheckoutTotal("AAAAA", 230);
    AssertSequenceCheckoutTotal("AAAAAA", 260);
    AssertSequenceCheckoutTotal("AAAB", 160);
    AssertSequenceCheckoutTotal("AAABB", 175);
    AssertSequenceCheckoutTotal("AAABBD", 190);
    AssertSequenceCheckoutTotal("DABABA", 190);
}

And it passes. Of course it does. Tests are awesome.

Ok I wasn’t totally honest. Remember I said I was scanning each item and adding up the total? It ended up being refactored out to a separate method. Let me highlight this area as it was very important and interesting:

  • Originally I had Scan() doing the total price calculation as items were added. I thought it would save me the compute time when calling CalculateTotalPrice()
    • I did this for awhile and the tests passed but turns out made the code a bit messy
    • The discount function had to check the discount based on the total tallied quantity which was not as straight forward as calculating in one go
  • Thats when I seperated the responsibilities of the total price calculation and scanning: scanning simply added the item to the dictionary, caluclation actually did the caculation
    • As a result Discount class was also simplified to use the entire quantity amount per item rather than incrementally add up the discounts
  • The coded ended up much cleaner and readable than before, and the code expresses exactly what the algorithm says it should do Remember that when your code reads like it does what it should do, then you’ve done a good a job.

The code is attached here Checkout.7z

Blog Comments powered by Disqus.

Next Post