...

/

Writing the First Property

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 ...

The property

property "sums without specials" do
  forall {item_list, expected_price, price_list} <- item_price_list() do
    expected_price == Checkout.total(item_list, price_list, [])
  end
end

The generators

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

  1. A list of items bought by the customer, item_list.
  2. The expected price of those items, expected_price.
  3. The list of items with their prices as expected by the register itself, price_list.

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:

defp item_price_list() do
  let price_list <- price_list() do
    let {item_list, expected_price} <- item_list(price_list) do
      {item_list, expected_price, price_list}
    end
  end
end

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 generator can then use price_list as the actual data structure rather than the abstract intermediary format PropEr uses. But first, let’s implement the price_list generator:

defp price_list() do
  let price_list <- non_empty(list({non_empty(utf8()), integer()})) do
    sorted = Enum.sort(price_list)
    Enum.dedup_by(sorted, fn {x, _} -> x end)
  end
end

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 Enum.dedup_by(enumerable, fun). This function will collapse all the consecutive duplicated elements into one element.

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

defp item_list(price_list) do
  sized(size, item_list(size, price_list, {[], 0}))
end

defp item_list(0, _, acc), do: acc

defp item_list(n, price_list, {item_acc, price_acc}) do
  let {item, price} <- elements(price_list) do
    item_list(n - 1, price_list, {[item | item_acc], price + price_acc})
  end
end

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:

def total(item_list, price_list, _specials) do 
  Enum.sum(
    for item <- item_list do 
      elem(List.keyfind(price_list, item, 0), 1)
    end
  )
end

Note: The property will work now.

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