Monday, October 26, 2015

Testing messages with Elixir


If you're coming to Elixir from another language, you might wonder how to assert that a process sent a message to another process.

The key is to remember that each test itself is also an Erlang process (and has its own pid).

We can send a message to the test process's mailbox, and then use `assert_receive` to assert we got the message.

For example:


test "something" do
  IO.puts "I am test, my pid is #{inspect self}"
  send(self, "hello test")

  assert_receive "hello test"
end


When you want to assert that a process sends a message, you just need to make sure the process is sending the messages back to the test's pid.

We do this by creating a stub that forwards messages.

Example


Say you have GenServer process that handles Payroll. Each time an employee is registered with the process, it sends a notification using GenEvent.
You want to assert that the event is sent.

This is what your Payroll server looks like:


defmodule Payroll do
  use GenServer

  # convenience function for starting up server, requires an event manager
  def start_link(manager) do
    # start GenServer with current module, and pass manager as state
    GenServer.start_link(__MODULE__, [%{events: manager}])
  end

  # initialize state
  def init([state]) do
    {:ok, state}
  end

  def handle_call({:add, employee}, _from, state) do
    # send out event
    GenEvent.notify(state.events, {:added, employee})

    # reply :ok
    {:reply, :ok, state}
  end

  # public API for adding employee
  def add_employee(pid, employee) do
    GenServer.call(pid, {:add, employee})
  end
end


It can be used like this:


# start a gen event server
{:ok, events} = GenEvent.start

# start the payroll service and pass the event manager's pid
{:ok, payroll} = Payroll.start_link(events)

# spawn another process that will print out events
spawn fn ->
  GenEvent.stream(events)
    |> Stream.each(&IO.inspect/1)
    |> Enum.to_list
end

# add an employee to the payroll service, which will result in notification being sent to previously spawned process
Payroll.add_employee(payroll, %{name: "Josh", salary: 1_000_000})


To test it, we'll need to create a stub GenEvent handler to forward messages back to the test:


# inside payroll_test.exs

# define a stub GenEvent handler
defmodule Forwarder do
  use GenEvent

  # handle all events, first parameter is the event, second if the state
  def handle_event(event, test_pid) do
    # send the event to `test_pid`
    send(test_pid, event)

    # respond `:ok` and keep the same state (the test's pid)
    {:ok, test_pid}
  end
end

# create all the processes we need in the setup
# ExUnit automatically terminates child processes after each test completes
setup do
  # create a GenEvent process
  {:ok, events} = GenEvent.start

  # start the payroll process, passing the event process's pid
  {:ok, payroll} = Payroll.start_link(events)

  # add the Forwarder as a handler, pass the current pid (self) as the state
  GenEvent.add_handler(events, Forwarder, self)

  # return test state
  {:ok, %{events: events, payroll: payroll}}
end

test "adding employee, sends notification", state do
  Payroll.add_employee(state.payroll, %{name: "Josh", salary: 1_000_000})

  assert_receive {:added, %{name: "Josh", salary: 1_000_000}}
end




And that is all folks..

Happy Elixiring!

2 comments:

Henrik N said...

ExUnit actually has assert_receive/assert_received which handle timeouts sensibly: http://elixir-lang.org/docs/v1.0/ex_unit/ExUnit.Assertions.html

I only know this because I wrote a similar blog post recently :) http://thepugautomatic.com/2015/09/testing-callbacks-in-elixir/

Josh said...

@Henrik, Didn't realize that, will update. Thanks for the feedback!