Ex_money_sql - Serializing JSON for tests

Hi, I am having some difficulty understanding how Jason and ex_money_sql can come together.

Basically my tests look a little weird: (but it is the only way I could make the tests work)

test "this is a test", %{conn: conn} do

assert json == %{
...
        "balance" => Money.new(:USD, "0.00") |> Jason.encode |> elem(1) |> Jason.decode |> elem(1),
        "escrow" => Money.new(:USD, "0.00") |> Jason.encode |> elem(1) |> Jason.decode |> elem(1),
...
end

The factory is as follows:

  def account_factory do
    %{
...
      balance: Money.new(:USD, "0.00"),
      escrow: Money.new(:USD, "0.00"),
...
    }
  end

The above values are stored correctly in the database as:
balance: (USD,0.00)
escrow: (USD,0.00)

Upon retrieval via JSON in my view:
balance: %{“amount” => “0.00”, “currency” => “USD”}
escrow: %{“amount” => “0.00”, “currency” => “USD”}

Which is as expected.

In my initial test, I assert it against the values of

        "balance" => Money.new(:USD, "0.00"),
        "escrow" => Money.new(:USD, "0.00"),

Which does not work. It only works if i write the assertion as above but I sense something is clearly wrong here. Will appreciate any directions.

Thank you.

PS: Is my usage of elem(1) correct or is it an antipattern to obtain the value of the second element of a {:ok, _} or {:error, _} tuple.

1 Like

Happy to help (I’m the author of ex_money_sql) but I’m a bit confused about what you are testing here since there isn’t any JSON involved in the code above? A couple of things to note:

  1. The ! versions of Jason.encode/1 and Jason.code/1 fit better with your first example:
assert json == %{
  "balance" => Money.new(:USD, "0.00") |> Jason.encode! |> Jason.decode!,
  ...
}
  1. You can do a full round trip from Money.t() → JSON → map → Money.t() with the following:
iex> Money.new(:USD, "0.00") |> Jason.encode! |> Jason.decode! |> Money.Ecto.Composite.Type.cast()
{:ok, #Money<:USD, 0.00>}
  1. As an aside, Money.new(:USD, "0.00") and Money.new(:USD, 0) return equivalent results. All money amounts, including those serialised in the database, are stored as decimal amounts with typically 28 digits of precision (as set by the Decimal context). Rounding to the right precision for a currency is done when calling Money.to_string/2 or when explicitly rounding with Money.round/2 or when splitting with Money.split/1
  2. Can you show an example of what json looks like in your test above, or a link to a repo?
2 Likes

Thinking on this a little more I think I understand what you’re trying to do. I suspect (not tested) that this might be the right approach:

test "that the json params are deserialised and a money struct resolved", %{conn: conn} do
  {:ok, balance} = Money.Ecto.Composite.Type.cast(conn.params["balance"])
  assert Money.equal?(balance, Money.new(:USD, 0))
end

Hi Kip, I very much appreciate your kind help and advice.

Silly me. Your Jason syntax was what I was looking for.

assert json == %{
  "balance" => Money.new(:USD, "0.00") |> Jason.encode! |> Jason.decode!,
  ...
}

My JSON code for the view:

  def render("account.json", %{account: account}) do
    %{
      account_id: account.account_id,
      balance: account.balance,
      escrow: account.escrow,
      inserted_at: account.inserted_at,
      updated_at: account.updated_at
    }

The test:

      assert json == %{
        "account_id" => json["account_id"],
        "balance" => Money.new(:USD, "0.00") |> Jason.encode! |> Jason.decode!,
        "escrow" => Money.new(:USD, "0.00") |> Jason.encode! |> Jason.decode!,
        "inserted_at" => json["inserted_at"],
        "updated_at" => json["updated_at"]
      }
  1. Yes - but only if Money.equal is done in your later post. If we are comparing the emitted JSON from the queries:
        "balance" => Money.new(:USD, 0) |> Jason.encode! |> Jason.decode!,
        "escrow" => Money.new(:USD, "0.00") |> Jason.encode! |> Jason.decode!,

Both yields very different results and will fail the assertion.

As my test requires pattern matching of ALL fields in the JSON obj generated by the view, i am unable to write a seperate test where i can

assert Money.equal?(balance, Money.new(:USD, 0)

This is the JSON object:

left:  %{"account_id" => 0, "balance" => %{"amount" => "0.00", "currency" => "USD"}, "escrow" => %{"amount" => "0.00", "currency" => "USD"},"inserted_at" => "2021-10-07T13:48:19.000000Z", "updated_at" => "2021-10-07T13:48:19.000000Z"}

independently. But I may be missing something here…

Please correct me if im mistaken. Thank you very much.

By the way, merely writing this:

        "balance" => Money.new(:USD, 0) |> Jason.encode! |> Jason.decode!,

results in a successful test.

My opinion (and it is opinion, not dogma) is that there is a better way to do this kind of test where you have two objectives:

  1. Validate the shape of the data
  2. Validate the type of the data

This is the kind of activity that embedded schemas are designed for. Therefore I think that a better way to test is like this:

defmodule Payload do
  @moduledoc "Define the shape of the data and the data types"
  use Ecto.Schema
  import Ecto.Changeset

  embedded_schema do
    field :account_id, :integer
    field :balance, Money.Ecto.Composite.Type
    field :escrow, Money.Ecto.Composite.Type
    timestamps()
  end

  def changeset(payload, fields) do
    %__MODULE__{}
    |> cast(payload, fields)
  end
end

And then the test can look like:

defmodule PayloadTest do
  use ExUnit.Case

  test "Casting a json payload" do
    test_payload =  %{
      "account_id" => 1,
      "balance" => %{"currency" => "USD", "amount" => 0},
      "escrow" => %{"currency" => "USD", "amount" => 0}
    }

    assert Payload.changeset(test_payload, [:balance, :escrow]).valid?
  end
end

I think this is more declarative, clearer in intent and much easier to maintain.

3 Likes