Testing Specials

Learn how to test the specials list in the checkout example and how to generate the final data required for the test.

Getting started

Rather than modifying the existing property, which does a fine job of checking non-special prices, we’ll add a new one to check specials. The separation will help narrow down problems when they happen. If the property for basic prices always works, then we’ll know that failures in the separate special property likely relate to bugs in the special one’s handling.

Let’s take a look at the property that we could implement.

prop_special() ->
    ?FORALL({ItemList, ExpectedPrice, PriceList, SpecialList},
            item_price_special(),
        ExpectedPrice =:= checkout:total(ItemList, PriceList, SpecialList)).

This property is similar to the one we wrote earlier, except that we now expect another term out of the generator, which is a list of special prices, SpecialList in Erlang. The easiest way to go about this would be to just come up with a static list of specials and then couple it with the previous property’s generator, but that wouldn’t necessarily exercise the code as well as fully dynamic lists, so let’s try to do that instead.

Planning the generators

First things first, we’ll need the basic list of items and prices. For that, we can reuse the price_list() generator, which gives us a fully dynamic list. Then, if we want the specials list to be effective, we should probably build it off the items in the price list. That can be done by wrapping the call to price_list() in a ?LET macro so that other generators can see it as a static value. It would look like:

?LET(PriceList, `price_list()`, 
  <<rest of the generator>>).

The challenge will be to generate the list of items to buy while maintaining a proper expected price, ExpectedPrice. Let’s say we were to reuse the item_list/1 generator from earlier here. If the checkout counter sells three donuts for the price of two, but the generator creates a list of four donuts without expecting a special, then our test will be wrong. If we were to patch things up by continuing to use the generator and figuring out the specials from the generated list, then chances are the generator code would be as complex as the actual program’s code. That’s a bad testing approach because it makes it hard to trust our tests.

Instead of building the list one item at a time and figuring out if or when specials apply, we can try to break it up by separately generating both types of sequences: items that never amount to a special, and items that always amount to a special. If these types of sequences are well-generated and distinct, then they can be used independently or as one big list, and always remain consistent.

So, we can have two generators. On the one hand, we have a list where all the items of each type are in a quantity smaller than required for the special price. We can sum up their prices to give us one part of the expected price. Then on the other side, we have a generator where all the items in a list are in a quantity that is an even multiple of the quantity required for a special. For that list, the expected price is the sum of all the specials that we triggered. We can then merge both lists, add both expected prices, and end up with a list that covers all kinds of cases possible in a totally predictable manner.

Let’s step through this with our example where three donuts are being sold at the price of two. If we have a list with eight donuts in it, we’d expect the special to be applied twice, with two remaining donuts being sold at full price. So, let’s see how this would work with our two planned generators. First, we generate a list of two donuts, which is below the special number, and track their expected price with the non-special value. Then we generate a list of six donuts (twice the special quantity) with their expected price based on the special value. Then, we merge both lists together and sum up their expected prices, and end up with the same eight donuts and the correct expected price.

This should work fine. The generator should look like this:

Get hands-on with 1400+ tech skills courses.