Assert_receive to test Genserver message handlers?

Hello,

I would like to be able to catch messages going to my GenServer’s handle_info in tests, to check those are what I intend to.

1/ Is there a way to print somehow every message coming through?
2/ Using assert_receive is there a way to catch those messages? Should I set the assert_receive before or after the call to the external service that will result in the handle_info trigger? What syntax should I use?

I tried many combinations of assert_receive and I tried a receive do... to try and display messages getting in, with no success.

1 Like

For #1: By far, one of my favorite troubleshooting features of our runtime is :sys.trace/2. You can turn it on in the course of launching your GenServer, by passing a list of atoms in the debug keyword option to GenServer.start_link/3, something along the lines of:

GenServer.start_link(MyModule, args, debug: [:trace])

Works for :gen_statem, too.

For #2, can you share as much as you can about your test setup, context and content? If you’re trying to intercept or observe the messages flowing to and from another process started during your test case, in order to make assertions about them, that probably isn’t going to work. assert_receive and receive blocks are both specifically for messages send to your own process, which is to say, the Elixir process running your test case. Anything that is started as a separate process (perhaps using start_supervised) won’t be subject to those calls.

One pattern I like to use is to make sure that the public API functions for a given GenServer takes a pid as one of the arguments, and fire those at my own test process via self(). Then I can use assert_receive to verify the shape of the message, as an independent test case from the inner logic it’s meant to trigger. Then I separately test the handle_* calls with good and bad arguments as normal function calls.

3 Likes

Hello and thanks, your answer is really helpfull!

I will try the :sys.trace/2 thing first thing tomorrow morning. As of my setup and code :

Here is my genserver’s code (code insights welcome as well, still not really comfortable with it) :

defmodule MyApp.MyExternalAppModule do
  use GenServer
  @external_app_node Application.get_env(:my_app, :external_app_node)
  @mailer Application.get_env(:my_app, :mailer)

  def start_link(_args) do
    GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
  end

  def insert(field1, field2, field3) do
    GenServer.call(__MODULE__, {:insert, field1, field2, field3})
  end

  def init(%{}) do
    {:ok, %{ref: nil}}
  end

  def handle_call(
        {:insert, _field1, _field2, _field3},
        _from,
        %{ref: ref} = state
      )
      when is_reference(ref) do

    {:reply, :ok, state}
  end

  def handle_call({:insert, field1, field2, field3}, _from, %{ref: nil}) do
    task =
      Task.Supervisor.async_nolink(
        {MyExternalApp.TaskSupervisor, @external_app_node},
        MyExternalApp.MyExternalAppModule,
        :my_function,
        [field1, field2, field3]
      )

    {:reply, :ok, %{field1: field1, field2: field2, field3: field3, ref: task.ref}}
  end

  def handle_info(
        {ref, {:ok, _external_element}},
        %{ref: ref, field1: field1, field2: field2, field3: field3} = state
      ) do
    Process.demonitor(ref, [:flush])

    @mailer.send_mail("(...)success")

    {:noreply, %{state | ref: nil}}
  end

  def handle_info(
        {ref, {:error, reason}},
        %{ref: ref, field1: field1, field2: field2, field3: field3} = state
      )
      when is_atom(reason) do
    Process.demonitor(ref, [:flush])

    @mailer.send_mail("(...)failure")

    {:noreply, %{state | ref: nil}}
  end

  def handle_info(
        {ref, {:error, _changeset}},
        %{ref: ref, field1: field1, field2: field2, field3: field3} = state
      ) do
    Process.demonitor(ref, [:flush])

    @mailer.send_mail("(...)failure")

    {:noreply, %{state | ref: nil}}
  end
end

Tests :

defmodule MyApp.MyExternalAppModuleTest do
  use ExUnit.Case, async: true

  @my_external_app_module Application.get_env(:my_app, :my_external_app_module)

  describe "insert/3" do
    test "when my_external_app node is up and the data exists returns (TODO)" do
      assert :ok == @my_external_app_module.insert("field1", "field2", "field3")
      assert_receive {_, {:ok, _}}, 3000
    end
  end
end

And you’re right, my GenServer is started by the Supervisor! Didn’t get that would be a problem…

1 Like

In addition to what @shanesveller said you can also set tracing during the test itself - I’ve found this to be very useful to assert that messages are sent and received and that complex inter process behaviour (like a chain of processes being created, completed and exiting) happens as expected .

The relevant docs are here: http://erlang.org/doc/man/erlang.html#trace-3 , there are many options you can give to trace that allow you to control it and customize its behaviour.

A sample would be

pid = GenServer.whereis(pid)
:erlang.trace(pid, true, [:receive])

:ok = GenServer.call(pid, :something)

assert_receive {:trace, ^pid, :receive, {:"$gen_call", _, :something}}
7 Likes

Hey ! Thanks, that’s interesting. Actually following your example I was able to test the received messages. Although, if I understand well, :erlang.trace(pid, true, [:receive]) shapes the message like {:trace, ^pid, :receive, {:"$gen_call", _, :something}}… I would like to understand what is the original form of the received message, if there is any way to listen to it without using :erlang.trace?

Also, @shanesveller suggested to start the GenServer like so:

GenServer.start_link(MyModule, args, debug: [:trace]) 

But what if it’s started by Supervisor.start_link? Can’t add debug: [:trace] then…

I had no success printing the full messages stack yet.

1 Like

Hi, I’m not sure there is, I’ve found trace and since it allowed me to test complex chains without needing to fiddle with the implementation of the processes themselves I didn’t search for anything else.

The form of the message is contained in the trace info, in case of a call {:"$gen_call", _, :something}, which means {:"$gen_call" = typeof_message, {pid, ref} = caller, :something = message_contents} (I think the second tuple is in that form but haven’t double checked). Of course testing calls only makes sense if the calls are made somewhere else than the test, because otherwise call's return and you can simply assert on the calling point that the received reply is correct.

If it’s a plain message you want to check then the trace format will be, for instance, {:trace, receiver_pid, :receive, {:DOWN, _, :process, process_pid, :normal}}, where the last tuple is the message as was delivered to the process that you’re tracing.

If an assert_receive fails ExUnit will truncate the displayed messages to 10 messages (this would be great to make customisable as right now it’s hardcoded in ExUnit) from the testing process mailbox, but you can see all the messages that you have in the mailbox at any given time with :erlang.process_info(self(), :messages) |> IO.inspect(limit: :infinity) (this can be an expensive op but given that it’s a test there’s no “issue” I think, I used this to figure out the form of the different trace messages I needed).

If you need to peek into the state of the process there’s also :sys.get_state(pid) (which can be useful for testing, if you need to confirm that the process state for instance changed, and also for debugging running programs).

5 Likes

So many usefull informations, that’s awesome. Thanks a lot.

2 Likes