< Home of 0x7f.dev

Unit testing SQL functions in an Elixir project

If you ever needed to add an SQL function that you can use in your Elixir project you might have skipped writing tests for it. There are a couple of official ways^1 ^2 you can write unit tests for PostgreSQL functions but having it in the same code base will make it so you actually maintain the tests.

Creating a function

First of all let's write an SQL function. Here is one I wrote a few days ago, converting a string representation of a number to either the decimal type or NULL.

CREATE OR REPLACE FUNCTION try_convert_to_decimal (v_input text)
  RETURNS DECIMAL
  AS $$
DECLARE
  v_dec_value DECIMAL DEFAULT NULL;
BEGIN
  BEGIN
    v_dec_value := v_input::decimal;
  EXCEPTION
  WHEN OTHERS THEN
    RETURN NULL;
  END;
  RETURN v_dec_value;
END;
$$
LANGUAGE plpgsql;

Easiest way of getting this into your Postgres database is by using a migration. You can find it in my example project^3. To create it just run:

$ mix ecto.gen.migrate add_function

In code you might be using this like:

from(
  ud in UserData,
  where: fragment("try_convert_to_decimal (?) >= ?", ud.value, ^some_value)
)

Testing

Testing this is very easy actually. And we can accomplish this by running raw SQL queries against our database by using Ecto^4. Here is the function that we will use in order to run our query:

def run_query(query) do
  Ecto.Adapters.SQL.query(TestingSqlFunctions.Repo, query, [])
end

This returns a Postgrex.Result struct that we can assert on:

%Postgrex.Result{
  columns: ["result"],
  command: :select,
  connection_id: 4618,
  num_rows: 1,
  rows: [[Decimal.new("100")]]
}

And so, a full test will look something like this^5:

test "converts a string integer to decimal" do
  query = "SELECT try_convert_to_decimal ('100') AS result;"
  result = Decimal.new("100")

  assert {:ok,
          %{
            columns: ["result"],
            rows: [[^result]]
          }} = run_query(query)
end

That's it! Now you won't have random changes or dropped migrations breaking your app 🎉! Do you have any better ideas on how to handle this? Reach out and together we can extend this post.