Jeremy Belolo
Jeremy Belolo

Reputation: 4539

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.

Upvotes: 1

Views: 216

Answers (2)

Jeremy Belolo
Jeremy Belolo

Reputation: 4539

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}}

Upvotes: 0

Tano
Tano

Reputation: 1377

I had similar problem to this one, but in the tests I did not use assert_receive, instead I have solved it through using Erlangs' :sys.get_state/1, where the argument you must pass is a pid(). This functions will wait until all messages in the mail box of the process are processed and then it will return the state of that process. So after getting the state you can compare with the values you were expecting to be changed.

Upvotes: 1

Related Questions