Search code examples
erlangelixirintegration-testinggen-server

How to integration_test that kind of genserver? Proper use of assert_receive?


I have an app remotely connected to another one’s node. The app needs to be able to call a distant function using this node. It works when called from iex, but I really struggle to get my integration tests right. I would like to check what is the return of the remote app, and if it fits what is expected.

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 doesn't exist returns (TODO)" do
      assert_receive {_, {:ok, _}}, 3000
      assert :ok == @my_external_app_module.insert("field1", "field2", "field3")
    end
  end
end

So assert_receive {_, {:ok, _}}, 3000 doesn’t work, obviously… I tried to mold it in a lot of ways without finding how it should work. What I want to do is check that it’s the right handle_info that is called and the data is as expected.

Mostly about assert_receive behavior that is.


Solution

  • The solution would be to trace incoming messages with something like

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

    And then you watch for messages with

    assert_received {:trace, ^pid, :receive, {:"$gen_call", _, :something}}
    

    Making sure the call is effective, and then

    :timer.sleep(100) # Just to make sure not tu run into a race condition
    assert_receive {:trace, ^pid, :receive, {ref, :returned_data}}