Testing callbacks in Elixir

Written . Tagged Elixir, ExUnit, Testing.

Say you have this code:

example.ex
1
2
3
4
5
6
defmodule Example do
  def run(callback) do
    callback.(:hello, :world)
    do_more_stuff
  end
end

You want to assert that it calls back with :hello and :world.

It might not be immediately clear how to do that in ExUnit.

example_test.exs
1
2
3
4
5
6
7
8
9
test "callback runs" do
  callback = fn (greeting, celestial_body) ->
    # ?
  end

  Example.run(callback)

  #?
end

We could assert inside the callback… but if the callback never runs, the assertion won’t run either.

In a language like Ruby, you could do it by changing a variable outside the anonymous function:

example_test.rb
1
2
3
4
did_it_run = false
fun = -> { did_it_run = true }
fun.()
assert did_it_run

In Elixir, an anonymous function can read variables from outside but not change them. We could start a separate server process and make it hang on to this state, but that would be a bit of a bother.

There are other ways to communicate, though. Message passing to the rescue!

example_test.exs
1
2
3
4
5
6
7
8
9
test "callback runs" do
  callback = fn (greeting, celestial_body) ->
    send self, {:called_back, greeting, celestial_body}
  end

  Example.run(callback)

  assert_received {:called_back, :hello, :world}
end

We simply send a message to our own process from the callback. Now it’s in our process mailbox.

Then we assert that we received it.

For multi-process use cases, you can name the test process:

example_test.exs
1
2
3
4
5
6
7
8
9
10
11
12
13
defmodule TestCallerBacker do
  def run(greeting, celestial_body) do
    send :test, {:called_back, greeting, celestial_body}
  end
end

test "callback runs" do
  Process.register self, :test

  Example.run_in_another_process(TestCallerBacker)

  assert_received {:called_back, :hello, :world}
end

assert_received expects the message to have arrived already. If your code is asynchronous and the message may take a while to arrive, its companion function assert_receive lets you specify a timeout.