August 22, 2016

Better Unit Testing with Tuplize

Elixir unit testing is fantastic, especially with the recently-added colored diff messages, but often it can get cumbersome to compare collections of Ecto models.

Better Unit Testing with Tuplize

Say you have a unit test that compares a list of structs.

# %User{city: nil, email: nil, first_name: nil, id: nil, last_name: nil, password: nil, state: nil, zipcode: nil}

users = [u_1, u_2, u_3, u_4, u5]
expected = [u_5, u_4, u_3, u_2, u_1]
assert users == expected

A simple list of 5 users out of order can blow up like

  1) test the truth (TuplizeTest)
     test/tuplize_test.exs:6
     Assertion with == failed
     code: users == expected
     lhs:  [%User{city: "48015 Daron Knolls", email: "[email protected]", first_name: "Dallas", id: 974, last_name: "Rosenbaum", password: "8550008389", state: "FL", zipcode: "64063-1288"}, %User{city: "26 Wilderman Path", email: "[email protected]", first_name: "Jude", id: 370, last_name: "Hagenes", password: "2757565087", state: "AZ", zipcode: "76720"}, %User{city: "88 Spinka Key", email: "[email protected]", first_name: "Estefania", id: 638, last_name: "Schmeler", password: "8238362474", state: "RI", zipcode: "09096"}, %User{city: "5 Joe Viaduct", email: "[email protected]", first_name: "Rachael", id: 558, last_name: "Hoppe", password: "2162383519", state: "GA", zipcode: "42240"}, %User{city: "86947 Monica Knolls", email: "[email protected]", first_name: "Chanel", id: 79, last_name: "Collier", password: "8587846213", state: "AL", zipcode: "97504-5221"}]
     rhs:  [%User{city: "86947 Monica Knolls", email: "[email protected]", first_name: "Chanel", id: 79, last_name: "Collier", password: "8587846213", state: "AL", zipcode: "97504-5221"}, %User{city: "5 Joe Viaduct", email: "[email protected]", first_name: "Rachael", id: 558, last_name: "Hoppe", password: "2162383519", state: "GA", zipcode: "42240"}, %User{city: "88 Spinka Key", email: "[email protected]", first_name: "Estefania", id: 638, last_name: "Schmeler", password: "8238362474", state: "RI", zipcode: "09096"}, %User{city: "26 Wilderman Path", email: "[email protected]", first_name: "Jude", id: 370, last_name: "Hagenes", password: "2757565087", state: "AZ", zipcode: "76720"}, %User{city: "48015 Daron Knolls", email: "[email protected]", first_name: "Dallas", id: 974, last_name: "Rosenbaum", password: "8550008389", state: "FL", zipcode: "64063-1288"}]
     stacktrace:
       test/tuplize_test.exs:22: (test)

Wouldn't it be better if we could limit the number of attributes compared? Something like a list of ids and emails?

Drop this into your test/test_helpers.exs

defmodule Compare do
  def tuplize(model, attrs \\ [])
  def tuplize(model, attrs) when is_list(model) do
    Enum.map(model, fn(x) -> tuplize(x, attrs) end)
  end
  def tuplize(model, attrs) do
    attrs
    |> Enum.map(fn(x) ->
        case x do
          x when is_function(x) -> x.(model)
          _ -> Map.from_struct(model)[x]
        end
      end)
    |> List.to_tuple
  end
end

Update your test.

import Compare
args = [:id, :email]
users = [u_1, u_2, u_3, u_4, u5]
expected = [u_5, u_4, u_3, u_2, u_1]
assert tuplize(users, args) == tuplize(expected, args)

Run your test again. The output is much cleaner.

  1) test the truth (TuplizeTest)
     test/tuplize_test.exs:6
     Assertion with == failed
     code: tuplize(users, args) == tuplize(expected, args)
     lhs:  [{441, "[email protected]"}, {569, "[email protected]"}, {573, "[email protected]"}, {131, "[email protected]"}, {231, "[email protected]"}]
     rhs:  [{231, "[email protected]"}, {131, "[email protected]"}, {573, "[email protected]"}, {569, "[email protected]"}, {441, "[email protected]"}]
     stacktrace:
       test/tuplize_test.exs:23: (test)

Tuplize is very simple, fetching the atom attributes from the structs it is given and returning a list of neatly contained tuples. Tuplize can even execute anonymous functions on the structs, allowing for things like full name comparisons.

import Compare
args = [:id, fn(user) -> "#{user.first_name} #{user.last_name}" end]
users = [u_1, u_2, u_3, u_4, u5]
expected = [u_5, u_4, u_3, u_2, u_1]
assert tuplize(users, args) == tuplize(expected, args)
  1) test the truth (TuplizeTest)
     test/tuplize_test.exs:6
     Assertion with == failed
     code: tuplize(users, args) == tuplize(expected, args)
     lhs:  [{472, "Saul Schmeler"}, {769, "Janick Ruecker"}, {680, "Merl Rempel"}, {478, "Katheryn Weimann"}, {378, "Albert Conroy"}]
     rhs:  [{378, "Albert Conroy"}, {478, "Katheryn Weimann"}, {680, "Merl Rempel"}, {769, "Janick Ruecker"}, {472, "Saul Schmeler"}]
     stacktrace:
       test/tuplize_test.exs:23: (test)

Happy unit testing!