Lessons learned

How to create a Postgrex extension

  1. add {:postgrex, ">= 0.0.0"} to mix.exs
  2. implement Postgrex.Extension's callbacks

    1. Postgrex.Extension.init/1
    2. Postgrex.Extension.matching/1
    3. Postgrex.Extension.format/1
    4. Postgrex.Extension.encode/1
    5. Postgrex.Extension.decode/1
  3. enable during compile time

     Postgrex.Types.define(
       PgMoney.PostgresTypes,
       [PgMoney.Extension],
       []
     )

    see Postgrex.Types.define/3 for more.

How to do property-based testing

  1. add {:propcheck, "~> 1.1", only: [:test, :dev]} to your mix dependencies.

  2. 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
  3. 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 and different paths, same destination

      [m, n] = DB.save_in_temp_table(conn, decimal) could also be seen as a test oracle or different paths, same destination. This is what happens:

                      [insert]
                       |    |
        [store as money]    [store as numeric]
                       |    |
                       [read]
                       |    |
                       m == n

      We can build upon Postgrex handling of Decimal as a PostgreSQL numeric and compare it with the corresponding moneyvalue. 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

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 the after 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:

    1. write properties relating to lossy operations
    2. 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.