ExUnit Async Genserver Testing (?)

I’ve got some GenServers that use async functions GenServer.cast.

Right now in my unit tests, I’m treating the async calls like synchronous calls. That works like 90% of the time, especially when the system is lightly loaded.

test "an async function" do
  MyModule.async_call_to_update_genserver_state(<newstate>)
  assert MyModule.get_state == <newstate>
end

Can someone suggest an ExUnit strategy for testing async calls that will work 100% of the time??

1 Like

If MyModule.get_state is reading the data from the GenServer, the previous async call does not matter because the synchronous get_state will still be processed after the async call. Unless you have the GenServer doing the writes and reading the data from elsewhere. In this case, it would be nice to have more information.

1 Like

Thanks for your response - I just realized that GenServer messages are processed in order, so sending a synchronous call (like MyModule.get_state) should flush all the pending async actions from the message queue. I’ll report back after I have studied this a bit more…

I believe I understand this timing issue. My code has a named supervisor with workers. The workers invoke an add_child method on the supervisor. If the supervisor is dead, the worker will fail.

Originally, I started the supervisor in a setup block: setup :start_supervisor which looked like

defp start_supervisor(_)
  MySupervisor.start_link
  :ok 
end

The problem was:

  • MySupervisor.start_link would fail intermittently (approx 1 in 50 times)
  • The error message was {:error, {:already_started, PID}}
  • I suspect the ‘already started’ PID was left over from the previous test run…
  • Immediately after the setup, the supervisor PID would die
  • Without a supervisor, the test would fail

My solution was to restart the supervisor if the error condition was detected:

defp start_supervisor(_) do
  case MySupervisor.start_link do
    {:ok, pid}     -> pid
    {:error, elem} ->
      IO.inspect(START_SUP_ERROR_A: elem)
      start_supervisor(:restart)
  end
  :ok
end

Perhaps the lingering supervisor is an ExUnit problem - perhaps not. I’m new with Elixir - I’m not sure.

AndyL - thank you for your question. I am having a similar issue as you are having.

My understanding is that each test runs in its own processes, and I believe that the same process that runs the test also invokes the setup callback. When the test process dies, any processes created by the test process will also die, including the processes created in the setup callback and in the test itself. (This works much the same way as how all processes supervised by a supervisor will die if the supervisor dies).

If you run your tests concurrently, and some processes are named, then we are going to run into conflicts, as multiple processes try to register with the same name. However, if we are not running the tests concurrently, then, ideally, test 1 should finish, that process and all processes it started should die, and test 2 should start.

Like you, I’m experiencing this “lingering process” issues, and my tests are trying to register a process with a name that already exists.

All you need to do is just explicitly GenServer.stop the process that you create in the test. Process death via links is async, so the process may still be around when the next case starts. GenServer.stop will force the process to wait until your named process is down.

1 Like

Other alternatives to the problem above is to not name the process in the first place. So instead of this:

setup do
  Foo.start_link(name: :always_bar)
  :ok
end

You can do:

setup do
  {:ok, pid} = Foo.start_link()
  {:ok, pid: pid}
end

test "works like xyz", %{pid: pid} do

If you need to name the process, one trick is to give it the same name as the test:

setup %{test: test} do
  Foo.start_link(name: test)
  :ok
end

test "works like xyz", %{test: test} do
7 Likes

@benwilson512 thank you for your help! Using GenServer.stop/1 fixed the issue.