When writing tests in Elixir, we often have subjects (in describe or test blocks) that reference functions
describe "UserHandler.delete_users/2" do test "deletes the user if they exist" do # ... end end
This reads nice, but has problems:
delete_users/2is renamed, deleted, moved, or changes its arity the subject is wrong
A better approach is to reference the actual function, not just a string.
describe inspect(&UserHandler.delete_users/2) do # ... end
By referencing the actual module and function, we make sure it exists. If anyone renames the function, or changes its arity, the compiler will tell us. No more manual fixing tests that became out of sync with the code base over time.
With this simple change, we gain another valuable benefit: Editor support. Many code editors allow us to jump to function definitions. This makes it easy to jump from a test to the tested function. It goes the other way around too: Editors often allow to jump from a function definition to its uses. This allows to find where a function is tested.
But there is a downside (dissappointed gasp from the audience). It doesn't read as nice. The
inspect call clutters the test subject, making it less readable.
describe inspect is just too much of a prefix before actually useful content.
Ideally, we could give functions directly to
describe &UserHandler.delete_users/2 do # ... end
That's neat, right?! But sadly, ExUnit doesn't like it much
== Compilation error in file test/demo.exs == ** (ArgumentError) describe name must be a string, got: &UserHandler.delete_users/2 (ex_unit 1.15.0-rc.2) lib/ex_unit/callbacks.ex:751: ExUnit.Callbacks.__describe__/4
Maybe we need to propose this idea to ExUnit -
test should not only allow
String inputs, but also functions or modules.
We can give
inspect a better name, just for test purposes by defining a macro in our
@doc ~S""" Inspects an expression in a test. Useful for test descriptions to make sure the tested function exists and is displayed nicely in test output. """ defmacro f(expr) do quote do inspect(unquote(expr)) end end
The much shorter name
f (for function) makes our tests read much nicer!
describe f(&UserHandler.delete_users/2) do # ... end
It has the same meaning (the
f macro is just doing an
inspect), but we get compile-time errors/warnings and editor support we wanted.
I'm using this pattern in my open source library for quite a while now and it works great.