3
votes

Trying to learn basic code testing with elixir and have trouble wrapping my head around testing async functions(genserver's handle_info and cast).

One way that works but feels wrong is using :timer.sleep(x) after casting async functions.

For example, imagine handle_cast that is processing list of items from state and dispatching calls to another module that will fetch data from external API and when done it will send message(api data) back to that same genserver.

What would be the best way to test this? Can I monitor genserver messages/mailbox inside the test?

edit: Another example, say in init I do this to get initial state from db:

Process.send_after(self(), :started, 0)

And :started looks like:

defhandleinfo :started, state: state, do: new_state(UserServerStarted.get_data(state))

Do I have any other option then :timer.sleep to check if UserServerStarted returned correct thing? I know I could just test that module/unit separately but I want to do full integration test.

Here is how I am doing it now:

... SETUP - Supervisor start UserServer...

  test "spin up server and inital tree after user registers", %
   {user: user} do
    :timer.sleep(500)
    assert %{"default" => [nil]} == GenServer.call(UserServer.via_tuple(user.id), :get)
  end
1
You can get mailbox messages using Process.info(pid, :messages) but that would not reliably work with GenServer as the GenServer would process the message as soon as it receives it unless it's busy handling a previous message.Dogbert

1 Answers

3
votes

I have struggled with a similar problem recently. I managed to work it out by registering the process that I want to keep track of in tests:

defmodule Foo
  def bar do
    pid = spawn fn -> consume(channel, tag, payload) end
    if !Process.whereis(:worker) do
      Process.register(pid, :worker)
    end
  end
end

And in your test:

test "bar" do
  # < Setup code that triggers your process under test goes here >

  # I put this here because I noticed that sometimes the process would not be
  # fast enough to be registered - this was a quick fix.
  :timer.sleep(100) 

  ref = Process.monitor(Process.whereis(:worker))

  # assert_receive will block the test until your registered process exits,
  # or 10 seconds have passed (then something is wrong with the external service perhaps and the test will fail)
  assert_receive {:DOWN, ^ref, :process, _, :normal}, 10000

  # At this point the process has exited, continue with whatever assertions you want to make
end