Testing Side Processes with Monitoring in Elixir

I recently wrapped up a new project for one of our clients. In that project, we start a supervised GenServer that sends an email and immediately terminates. This process records the email being sent before terminating normally.

We had passing tests but in most test runs we had a big ugly Ecto sandbox warning because the test finished and terminated before the recording that the email was sent. Not great. Below is a simplified version of what that process looks like.

defmodule App.EmailSender do
  use GenServer
  
  def start_link(), do: ...
  
  def init(_) do
    {:ok, %{}, {:continue, :deliver}}
  end
  
  def handle_continue(:deliver, state) do
    # do the delivery
    
    # record what happened, this is where it explodes in tests
    
    {:stop, :normal, state}
  end
end

In order to get around this, I added in a Process.monitor to the PID that just launched.

With the processed monitored, we'll receive a :DOWN message on process termination. We can then assert_receive on that message to let the test process pause just long enough to make sure everything stays green and ERROR free. This works because assert_receive blocks for up to 100ms (before failing) letting the other process send and record without issue.

defmodule App.EmailTest do
  use ExUnit.Case
  
  alias App.Email
  
  test "sending an email" do
    email = create_email()
    
    {:ok, pid} = Email.deliver(email)
    ref = Process.monitor(pid)
    
    # assert whatever else

    assert_receive {:DOWN, ^ref, :process, _object, :normal}
  end
end

With this in place for every test that uses that process, we can now make sure the delivery process isn't outpaced by the test process in regards to the Ecto sandbox.

Header photo by Dean Brierley on Unsplash