Writing the First Property
Get started with the example by writing the first property.
We'll cover the following...
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:
- A list of items bought by the customer,
item_list
. - The expected price of those items,
expected_price
. - 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: