Writing the First Property

Get started with the example by writing the first property.

Getting started

The first property doesn’t have to be advanced. In fact, it’s better if it’s simple. Start with something trivial-looking that represents how we want to use the program. Then, our job as developers is to make sure we can write code that matches or changes our expectations. Of our two properties, the simplest one concerns counting sums without caring about specials.

We’ll want to avoid a property definition, such as sum(ItemList, PriceList) =:= checkout:total(ItemList, PriceList, []), since that would risk making the test similar to the implementation. A good approach to try here is generalizing regular example-based tests. Let’s imagine a few cases:

20 = checkout:total(["A","B","A"], [{"A",5},{"B",10}], []),
20 = checkout:total(["A","B","A"], [{"A",5},{"B",10},{"C",100}], []), 
115 = checkout:total(["F","B","C"], [{"F",5},{"B",10},{"C",100}], []),

That’s actually tricky to generalize. It’s possible that, to come up with examples, we just make a list of items, assign them prices, pick items from the list, and then sum them up ourselves. Even if it’s not really straightforward, we can build on that. The base step here looks something like this:

ExpectedPrice = checkout:total(ChosenItems, PriceList, [])

If we write a generator that gives us all known values for these variables, we can use the steps the same way we would with examples. The generator will generate test cases so that we don’t have to. So, let’s take a look at the properties, generators, and the implementation of the first test.

Let’s take a look at the Erlang code.

The property

prop_no_special1() ->
    ?FORALL({ItemList, ExpectedPrice, PriceList}, item_price_list(),
            ExpectedPrice =:= checkout:total(ItemList, PriceList, [])).

The generators

The generator for the property will need to generate the three expected arguments:

  1. A list of items bought by the customer, ItemList.
  2. The expected price of those items, ExpectedPrice.
  3. The list of items with their prices as expected by the register itself, PriceList.

Since the price list is required to generate the item list and expected prices, the generator will need to come in layers with ?LET macros:

item_price_list() ->
    ?LET(PriceList, price_list(),
         ?LET({ItemList, ExpectedPrice}, item_list(PriceList),
              {ItemList, ExpectedPrice, PriceList})).

The price list itself is a list of tuples of the form [{ItemName, Price}]. The ?LET macro actualizes the list into one value that won’t change for the rest of the generator. This means that the item_list/1 generator can then use PriceList as the actual data structure rather than the abstract intermediary format PropEr uses. But first, let’s implement the price_list generator:

price_list() ->
    ?LET(PriceList, non_empty(list({non_empty(string()), integer()})),
         lists:ukeysort(1, PriceList)). % remove duplicates

Here, price_list generates all the tuples as mentioned earlier, each with an integer for the price. To avoid duplicate item options, such as having the same hotdogs at two distinct prices within the same list, we use lists:ukeysort(KeyPos, List). That function will remove all list items that share the same key as any item that has already been seen, ensuring we have only unique entries.

Now we’ll use the PriceList as a seed for item_list/1, which should return a complete selection of items along with their expected price like this:

item_list(PriceList) ->
    ?SIZED(Size, item_list(Size, PriceList, {[], 0})).

item_list(0, _, Acc) -> Acc;
item_list(N, PriceList, {ItemAcc, PriceAcc}) ->
    ?LET({Item, Price}, elements(PriceList),
         item_list(N-1, PriceList, {[Item|ItemAcc], Price+PriceAcc})).

For the tests to pass, we’ll have to write the implementation code itself.

The implementation

Let’s start with a minimal case that should easily work. It would be something like this:

-module(checkout).


-export([total/3]).

-type item() :: string().
-type price() :: integer().

-spec total([item()], [{item(), price()}], any()) -> price(). 
total(ItemList, PriceList, _Specials) ->
    lists:sum([proplists:get_value(Item, PriceList) || Item <- ItemList]).

Note: The property will work now.

Let’s test it out in the code widget below:

Get hands-on with 1200+ tech skills courses.