Lessons learned
How to create a Postgrex extension
- add
{:postgrex, ">= 0.0.0"}
tomix.exs
implement
Postgrex.Extension
's callbacksenable during compile time
Postgrex.Types.define( PgMoney.PostgresTypes, [PgMoney.Extension], [] )
see
Postgrex.Types.define/3
for more.
How to do property-based testing
add
{:propcheck, "~> 1.1", only: [:test, :dev]}
to your mix dependencies.in your tests, for example
property_test.exs
:defmodule My.PropertyTest do use PropCheck setup do test_ctx = nil test_ctx end property "name", prop_cfg, test_ctx do # do prop-testing end end
do property-based testing :P
understood how
PropCheck.collect/3
works:test = expected == actual test |> collect( with_title("how to collect and set a header"), my_sample ) |> collect( with_title("chain and collect"), more_samples )
To my first surprise this results in:
chain and collect ... how to collect and set a header ...
Turns out to be right, because the equivalent looks like this:
collect( collect( test, with_title("how to collect and set a header"), my_sample ), with_title("chain and collect"), more_samples )
properties I tested:
symmetry or there and back again:
# lib level expected = 2 ^expected = expected |> PgMoney.Extension.to_dec() |> PgMoney.Extension.to_int() # db level # saving involves (hopefully) encoding and decoding [m, n] = DB.save_in_temp_table(conn, decimal) assert Decimal.eq?(decimal, n) assert Decimal.eq?(decimal, m)
test oracle
anddifferent paths, same destination
[m, n] = DB.save_in_temp_table(conn, decimal)
could also be seen as atest oracle
ordifferent paths, same destination
. This is what happens:[insert] | | [store as money] [store as numeric] | | [read] | | m == n
We can build upon
Postgrex
handling ofDecimal
as a PostgreSQLnumeric
and compare it with the correspondingmoney
value. Based on this principle, or at least how I implemented it, we go different paths and arrive at the same destination.
more:
How to do telemetry
- add
{:telemetry, "~> 0.4.0"}
to your mix dependencies. - emit an event with
:telemetry.execute/2
or:telemetry.execute/3
- to listen to an event you have to attach a handler via
:telemetry.attach/4
or:telemetry.attach_many/4
- if there is no interest in receiving the attached events you
:telemetry.detach/1
your:telemetry.handler_id()
- if you want to correctly measure elapsed time use
:erlang.monotonic_time/0
and:erlang.monotonic_time/1
- based on my test I would like to remove the
:start
and:stop
events as the duration is always0 ns
on my computer. - read more on Samuel Mullen's blog post The "How"s, "What"s, and "Why"s of Elixir Telemetry
Follow up
learn when
:erlang
does a process context switch.in my humble understanding that I have right now, I wanted to avoid having this during instrumentation. Hence I emit the
:start
and:stop
events inside theafter
block like this:def to_dec(integer, precision, telemetry) do started_at = current_time() try do case {PgMoney.is_money(integer), PgMoney.is_precision(precision)} do # implementation end after duration = time_diff(started_at, current_time()) emit_start(telemetry, :to_dec, started_at) emit_stop(telemetry, :to_dec, duration) end end
extend and write better properties
Right now I have only touched the surface with
within_precision_test.exs
. The next steps would be to:- write properties relating to lossy operations
- generalize it to any precision
I built the foundation to implement a ledger who tracks rounding losses like laid out in Mathias Verraes' article
Emergent Contexts through Refinement
found in DDD - The First 15 Years. But this is a topic for another time.