Using MSpec to Solve Kata Potter Part 1: The Specifications

Kata Potter is an interesting problem intended to be used as a learning exercise in TDD. The problem is described like this:

Once upon a time there was a series of 5 books about a very English hero called Harry. (At least when this Kata was invented, there were only 5. Since then they have multiplied) Children all over the world thought he was fantastic, and, of course, so did the publisher. So in a gesture of immense generosity to mankind, (and to increase sales) they set up the following pricing model to take advantage of Harry's magical powers.

One copy of any of the five books costs 8 EUR. If, however, you buy two different books from the series, you get a 5% discount on those two books. If you buy 3 different books, you get a 10% discount. With 4 different books, you get a 20% discount. If you go the whole hog, and buy all 5, you get a huge 25% discount.

Note that if you buy, say, four books, of which 3 are different titles, you get a 10% discount on the 3 that form part of a set, but the fourth book still costs 8 EUR.

Potter mania is sweeping the country and parents of teenagers everywhere are queuing up with shopping baskets overflowing with Potter books. Your mission is to write a piece of code to calculate the price of any conceivable shopping basket, giving as big a discount as possible.
I have been working on solving this kata over the last couple weeks. One goal I had was to understand how Machine.Specifications (MSpec for short) fits in to the BDD development process. MSpec is a framework written by Aaron Jensen that sits on top of your current unit testing framework (as long as you happen to be using NUnit or XUnit). You can read Aaron’s introduction to MSpec here where he does a much finer job at explaining it that I can.

My goal with MSpec was to be able to define the acceptance criteria for Kata Potter before writing a single line of implementation code. MSpec also has the added benefit of allowing me to execute my acceptance criteria on the code as it evolves and report back on my progress. With MSpec I can express what the system under test must do to satisfy all the requirements.

So given MSpec’s utility I started out to write a set of requirements based on the Kata Potter description.

  • Given a cart containing a single book when the price is calculated it should return the full price of the book.

  • Given a cart containing two different books when the price is calculated it should return the price of both books with a 5% discount applied.

  • Given a cart containing three different books when the price is calculated it should return the price of all books with a 10% discount applied.

  • Given a cart containing four different books when the price is calculated it should return the price of all books with a 20% discount applied.

  • Given a cart containing five different books when the price is calculated it should return the price of all books with a 25% discount applied.
    This covers the full set of books, but the kata gives us another example:

  • Given a cart containing four books where two are the same title when the price is calculated it should return price of three books discounted by 10% and the full price of the fourth.
    Using MSpec I can express these requirements in an executable fashion like this:

using Machine.Specifications;

namespace Kata.Potter.CoreTests.Specifications
{
    [Subject("calculating discount prices")]
    public class when_calculating_the_price_of_a_book
    {
        It should_return_the_full_price_of_the_book;
    }

    [Subject("calculating discount prices")]
    public class when_calculating_the_price_of_two_different_books
    {
        It should_apply_a_five_percent_discount;
    }

    [Subject("calculating discount prices")]
    public class when_calculating_the_price_of_three_different_books
    {
        It should_apply_a_ten_percent_discount;
    }

    [Subject("calculating discount prices")]
    public class when_calculating_the_price_of_four_different_books
    {
        It should_apply_a_twenty_percent_discount;
    }

    [Subject("calculating discount prices")]
    public class when_calculating_the_price_of_five_different_books
    {
        It should_apply_a_twenty_five_percent_discount;
    }

    [Subject("calculating discount prices")]
    public class when_calculating_the_price_of_four_books_where_two_are_the_same
    {
        It should_apply_a_twenty_percent_discount_to_three_of_the_books;
    }
}

With out a single line of code implementing these specifications, I can even execute my tests. I get the following display in the ReSharper test runner:

KataSpecs

Notice that the runner ignores all my tests currently returning the message that the specifications are “Not implemented”. As I write code to satisfy the requirements, the tests will being to go red and then green as each specification is implemented.

The next step in this process is to begin identifying collaborators in the system. Based on the requirements I know that I need objects that represent a Book, a Shopping Cart and a Calculator that calculates the price of all the books in the cart. I’ll begin by creating shell classes that I can use to wire up my specifications to implementation code.

public class Book
    {
        public Book(string title, decimal price)
        {
            Title = title;
            Price = price;
            IsDiscounted = false;
        }

        public string Title { get; private set; }
        public decimal Price { get; set; }
        public bool IsDiscounted { get; set; }
    }

    public class Cart
    {
        public Cart()
        {
            Books = new List<Book>();
        }

        public IList<Book> Books { get; private set; }

        public void AddBook(Book book)
        {
            Books.Add(book);
        }
    }

    public class PriceCalculator
    {
        public decimal CalculatePriceFor(Cart cart)
        {
            return 0;
        }
    }

With this simple implementation, I can now modify my MSpec specifications to exercise these objects. I’ll start with the first one to give you the feel of what it looks like.

[Subject("calculating discount prices")]
    public class when_calculating_the_price_of_a_book
    {
        private static Cart cart;
        private static PriceCalculator calculator;
        private static decimal price;

        Establish context = () =>
                                {
                                    cart = new Cart();
                                    cart.AddBook(new Book("Book 1", 8M));
                                    calculator = new PriceCalculator();
                                };
        Because of = () => price = calculator.CalculatePriceFor(cart);

        It should_return_the_full_price_of_the_book = () => price.ShouldEqual(8);
    }

First, I establish my specification’s context by newing up my Cart and PriceCalculator classes and adding a single book to the cart. This establishes the environment that the test will run in which matches the name of the class pretty closely. I am calculating the price of a single book. Next, I clearly call out what action is performed to test against. I tell the calculator to calculate the price for the given cart. Finally, I make assertions on the state of my context objects. For this specification, all I need to do is validate that the calculated price is equal to the price of the book.

If I execute my specifications now, this spec will turn from ignored to red. I have now implemented the collaborators and weaved them into specifications to be validated. Here is what shows up in the ReSharper runner:

kataspecs2

The next thing we need to do is fill in the rest of the specifications and make them all turn red as well. If I do this following the pattern of the test we already have I am going to create a lot of duplicated code. Each specification class will have the same private members and most of the context as well. So first I am going to pull those elements out into a base class and refactor my first specification like so:

    public class with_price_calculator
    {
        protected static Cart cart;
        protected static PriceCalculator calculator;
        protected static decimal price;
        protected static decimal correctPrice;

        Establish context = () =>
                            {
                                cart = new Cart();
                                calculator = new PriceCalculator();
                            };
    }

    [Subject("calculating discount prices")]
    public class when_calculating_the_price_of_a_book : with_price_calculator
    {
        Establish context = () =>
                                {
                                    cart.AddBook(new Book("Book 1", 8M));
                                };
        Because of = () => price = calculator.CalculatePriceFor(cart);

        It should_return_the_full_price_of_the_book = () => price.ShouldEqual(8);
    }

One of the cooler features of MSpec is the way it chains delegates together during execution. Notice that my base class has an Establish statement and my specification class has one as well. The specification class inherits from the base class but it does not override the Establish statement. Instead MSpec will execute all the Establish statements in the inheritance chain from the “base-ist” to the “descended-ist”. I can now implement the remaining specification quickly like this:

public class with_price_calculator
{
    protected static Cart cart;
    protected static PriceCalculator calculator;
    protected static decimal price;
    protected static decimal correctPrice;

    Establish context = () =>
                        {
                            cart = new Cart();
                            calculator = new PriceCalculator();
                        };
}

[Subject("calculating discount prices")]
public class when_calculating_the_price_of_a_book : with_price_calculator
{
    Establish context = () =>
                            {
                                cart.AddBook(new Book("Book 1", 8M));
                            };
    Because of = () => price = calculator.CalculatePriceFor(cart);

    It should_return_the_full_price_of_the_book = () => price.ShouldEqual(8);
}

[Subject("calculating discount prices")]
public class when_calculating_the_price_of_two_different_books : with_price_calculator
{
    Establish context = () =>
    {
        cart.AddBook(new Book("Book 1", 8M));
        cart.AddBook(new Book("Book 2", 8M));
        correctPrice = 8*2 - (8*2*.05M);
    };

    Because of = () => price = calculator.CalculatePriceFor(cart);

    It should_apply_a_five_percent_discount = () => price.ShouldEqual(correctPrice);
}

[Subject("calculating discount prices")]
public class when_calculating_the_price_of_three_different_books : with_price_calculator
{
    Establish context = () =>
    {
        cart.AddBook(new Book("Book 1", 8M));
        cart.AddBook(new Book("Book 2", 8M));
        cart.AddBook(new Book("Book 3", 8M));
        correctPrice = 8 * 3 - (8 * 3 * .1M);
    };

    Because of = () => price = calculator.CalculatePriceFor(cart);

    It should_apply_a_ten_percent_discount = () => price.ShouldEqual(correctPrice);
}

[Subject("calculating discount prices")]
public class when_calculating_the_price_of_four_different_books : with_price_calculator
{
    Establish context = () =>
    {
        cart.AddBook(new Book("Book 1", 8M));
        cart.AddBook(new Book("Book 2", 8M));
        cart.AddBook(new Book("Book 3", 8M));
        cart.AddBook(new Book("Book 4", 8M));
        correctPrice = 8 * 4 - (8 * 4 * .2M);
    };

    Because of = () => price = calculator.CalculatePriceFor(cart);
    It should_apply_a_twenty_percent_discount = () => price.ShouldEqual(correctPrice);
}

[Subject("calculating discount prices")]
public class when_calculating_the_price_of_five_different_books : with_price_calculator
{
    Establish context = () =>
    {
        cart.AddBook(new Book("Book 1", 8M));
        cart.AddBook(new Book("Book 2", 8M));
        cart.AddBook(new Book("Book 3", 8M));
        cart.AddBook(new Book("Book 4", 8M));
        cart.AddBook(new Book("Book 5", 8M));
        correctPrice = 8 * 5 - (8 * 5 * .25M);
    };

    Because of = () => price = calculator.CalculatePriceFor(cart);

    It should_apply_a_twenty_five_percent_discount = () => price.ShouldEqual(correctPrice);
}

[Subject("calculating discount prices")]
public class when_calculating_the_price_of_four_books_where_two_are_the_same : with_price_calculator
{
    Establish context = () =>
    {
        cart.AddBook(new Book("Book 1", 8M));
        cart.AddBook(new Book("Book 2", 8M));
        cart.AddBook(new Book("Book 3", 8M));
        cart.AddBook(new Book("Book 3", 8M));
        correctPrice = (8 * 3 - (8 * 3 * .1M)) + 8;
    };

    Because of = () => price = calculator.CalculatePriceFor(cart);

    It should_apply_a_ten_percent_discount_to_three_of_the_books = () => price.ShouldEqual(correctPrice);
}

Note that I have added another member to the base class to capture what I think the correct price should be to then use as an assert later on. Executing my specifications now show all red. I can continue implementing functionality using traditional TDD or by writing more specifications for each collaborator using the same techniques.

Next time, I will cover the actual implementation that solves the kata potter that I arrived at through this process.

Follow me on Mastodon!