You’re a new Elixir developer, excited to learn the language and join the community. You’ve written some code for an early project you want to expand and release as your first open-source Elixir package. 🎉
This package will be a plug (middleware) that loads resources from Ecto (the database wrapper). When used in an app, your library will use the app’s database connection. In your tests, though…well, actually, how do you test an Elixir package that uses the database?
At first, you look into setting up a database connection for your tests. You have plenty of time until your name is up and getting a database connection ought to be straightforward. It turns out, though, that Ecto repos seem to require some package-level setup and you’re not sure what implications that has.
Do you define your whole package as an OTP app, even though you only need this for testing? Since OTP apps can each have their own database connections, how will that affect users of your open-source project? Do you really want to force database setup onto possible contributors?
Whew. Maybe it would be easier to just not use a database after all! Maybe you can just compare the query itself with one your know is correct?
test "looks up the query appropriately" do conn = make_connection_with_plug(LoadResource.Plug) query = TestRepo.last_query expected_query = from row in TestModel, where: row.id == ^(id) assert query == expected_query end
Turns out Ecto includes the file and line on which each part of a query gets defined. This tracking (first introduced here) makes sense for exceptions, but makes your job harder: two queries that are functionally identical aren’t directly comparable.
You cast around for better ways to handle this — maybe you can cast the query to SQL? Nope 😕: it appears SQL compilation requires an established database connection, exactly what you want to avoid.
In your innocence, knowing better but not knowing how to do better, you hack together some gloriously awful and brittle code. You’re pretty sure you shouldn’t recommend this approach to anyone else (not that you’re allowed to talk to anyone else in here), but whatever — it works!
def assert_query_equality(query, expected_query) do remove_code_info_from_query = fn(query) -> # Turn the query struct into a regular map query = Map.from_struct(query) wheres = query[:wheres] # drop the file and line values from any where clauses cleansed_wheres = Enum.map(wheres, fn(clause) -> Map.drop(clause, [:file, :line]) end) # update the map with those cleaned expressions Map.put(query, :wheres, cleansed_wheres) end assert remove_code_info_from_query.(query) == remove_code_info_from_query.(expected_query) end test "looks up the query appropriately" do conn = make_connection_with_plug(LoadResource.Plug) query = TestRepo.last_query expected_query = from row in TestModel, where: row.id == ^(id) assert_query_equality(query, expected_query) end
You reflect, as you hear your name and make your way forward, on a key difference between functional and object-oriented languages. In an OO language like Ruby, objects have opaque internal states and so it’s natural to define an equality method — indeed, Rails defines
== for ActiveRecord::Relationship.
In a functional language where all objects are transparent dictionaries, arrays, etc., comparing objects is straightforward. There’s no urgent need to define a separate function. An OO implementation of Ecto would presumably define query equality, which would almost certainly ignore the file and line numbers; Elixir’s implementation doesn’t (yet1).
“Are equality methods considered a good practice in Elixir, a code smell, or neither?”, you say to yourself (only in your head, of course). “One more thing on the long, long list of things to learn.”
As you exit, momentarily distracted by the bright sunlight, you think how nice it will be to know how the answer to this someday2.
on the one hand, Elixir is relatively new; perhaps an equality method will exist someday. For what it’s worth (not much),
ActiveRecord::Relationship#==is was added in 2010, having originally been added to
NamedScopein an earlier 2010 commit ironically signed off by Jose Valim, creator of Elixir. For those counting, it took Rails 6 years to add equality; Ecto is currently 4 years old; these numbers really don’t matter but were fun to research. ↩