Love the pipe operator? Miss them in tests with ExUnit
when the result is derived from a pipeline of operations necessitating an intermediate variable, like result
? I did...
typical test assertion (no pipe operator)
defmodule AdderTest do
test "add" do
result =
1..3
|> Enum.map(& &1 * 2) # [2, 4, 6]
|> Adder.add(1) # [3, 5, 7]
assert result == [3, 5, 7]
end
end
verbose (with pipe)
We can employ an anonymous function (lambda). NOTE: invoked with dot-syntax
defmodule AdderTest do
test "add" do
1..3
|> Enum.map(& &1 * 2)
|> Adder.add(1)
|> (fn result -> assert result == [3, 5, 7] end).()
end
end
terse (with pipe)
This uses Elixir's & (capture operator) to keep things concise
defmodule AdderTest do
test "add" do
1..3
|> Enum.map(& &1 * 2)
|> Adder.add(1)
|> (& assert &1 == [3, 5, 7]).()
end
end
an option for those that use Elixir's formatter
If you or your team use Elixir's formatter, then you're going to end up with something like the following where the nice whitespace that gives your arguments some room to breathe is removed. One thing that I've done is add two enhancement functions (using macros) into test/test_helper.exs
that become available to tests by invoking use TestHelper
defmodule AdderTest do
test "add" do
1..3
|> Enum.map(& &1 * 2)
|> Adder.add(1)
|> (&assert(&1 == [3, 5, 7])).() # 🤮
end
end
Here's what we can do
# test/test_helper.exs
ExUnit.start()
defmodule TestHelper do
defmacro __using__(_opts) do
quote do
import ExUnit.Assertions, only: [assert: 1]
# we can name this whatever we'd like,
# but "is" makes sense to me in most cases
# 👇
def is(result, expectation) do
assert result == expectation
result # 👈 allows us to continue chaining assertions in a pipeline
end
# this one allows us to make more complex assertions
# e.g., asserting that a nested key is of a particular value
def has(result, assertion) when is_function(assertion) do
assert assertion.(result) == true
result
end
end
end
end
# test/adder_test.exs
defmodule AdderTest do
use TestHelper # 👈 included here
test "add" do
1..3
|> Enum.map(& &1 * 2)
|> Adder.add(1)
|> is([3, 5, 7]) # 👈 used here
|> has(&List.last(&1) == 7) # 👈 and here (chained)
end
end
Top comments (8)
Seems like you could take it a step further and return
result
in theis
function to pipe multiple assertionsYou also made me think up some more clever things to try, like returning a partially applied function, such that we can pipe not only values into the assertion but do more complex operations/assertions with a function (e.g. make assertions on nested fields)
Ah! Why didn't I think of this!? Yes! Clever clever! Thanks, Kevin!
This is cool. Although, I am pretty sure if you have a group of developers working on a project they will still write the typical one. 😁 And also there are some other things to assert like pattern matching. But still, this looks neat. Thanks for sharing
Yup! I was just looking for a way to make my tests a bit more concise in a personal project and decided to dig a little before I came up with this, but I do believe that should this be included, whether or not it is standardized in a test suite wouldn't make people balk :)
Oo I like that a lot, nice
Great article David!
I'll start to adopt it on my projects.
I miss work with you
Hey man!!! Me, too! Would be nice to hang out once the crisis is over. Have you updated your book? I recently told someone about it that is starting Elixir. I wrote this recently when I started playing around with Elixir again (almost two years) :D