Stop a GenServer after each test

Background

I have a set of tests that need a GenServer to be started before. As a rule of thumb, I understand it is a good practice to cleanup after each test, so I also want to stop the GenServer after each test.

Problem

The problem here is that I don’t know how to stop a GenServer after the test has finished. I always end up with some concurrency issue.

defmodule MyModuleTest do
  use ExUnit.Case

  alias MyModule

  setup do
    MyModule.Server.start_link(nil)
    context_info = 1
    more_info = 2
    %{context_info: context_info, more_info: more_info}
  end

  describe "some tests" do
    test "returns {:ok, order_id} if order was deleted correctly", context do
      # do test here that uses created server  and passed context
      assert actual == expected

      #clean up?
    end
  end
end

Now, I have tried on_exit/2 like the following:

  setup do
    {:ok, server} = MyModule.Server.start_link(nil)
    context_info = 1
    more_info = 2
    
    on_exit(fn -> GenServer.stop(server) end)

    %{context_info: context_info, more_info: more_info}   
  end

But i get this error:

** (exit) exited in: GenServer.stop(#PID<0.296.0>, :normal, :infinity)
         ** (EXIT) no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't started

I have the feeling this is exiting too soon.

Question

How can I fix this?

1 Like

Use start_supervised/{1,2}, and it will handle proper shutdown for you.

@hauleth Should I use it inside a setup block or setup_all?

I tried using start_supervised in a setup block like the following:

setup do
    start_supervised(MyModule.Server)
    context_info = 1
    more_info = 2
    %{context_info: context_info, more_info: more_info}
  end

However now I get another error:

** (exit) exited in: GenServer.call(MyModule.Server, {:delete_order, "5ee71a2604d55c0a5cbdc3c2"}, 5000)
         ** (EXIT) an exception was raised:
             ** (ArgumentError) argument error
                 (jobs 0.9.0) /market_manager/deps/jobs/src/jobs_server.erl:236: :jobs_server.call/3
                 (auction_house 1.0.0) lib/my_module/server.ex:34: MyModule.Server.handle_continue/2
                 (stdlib 3.10) gen_server.erl:637: :gen_server.try_dispatch/4
                 (stdlib 3.10) gen_server.erl:388: :gen_server.loop/7
                 (stdlib 3.10) proc_lib.erl:249: :proc_lib.init_p_do_apply/3

Looks like it has issues with my handle_continue, could it be?

Looks like it. Handle_continue is run after the process is started (opposed to while it‘s starting) so your test might try to run, while your genserver is still blocked doing whatever you do in that handle_continue callback, which makes the call initiated by your test time out.

Indeed I think so as well. My handle_continue sets up a queue and does some heavy lifting, and it looks like the tests are running before the GenServer setup is ready.

How could I fix this?

Either make the setup the process does sync (put it in init instead of a handle_continue) or add something into you test setup, which calls into your genserver and wait for it to finish (which also means your setup is finished).

1 Like

The first solution is a big no for me. I use handle_continue because the GenServer initialization is long and this is, afaik, the recommended solution for such cases.

The second solution however… how would you implement it? By adding a ping call to the GenServer or something similar?

Aren’t there any other possible solutions that don’t use start_supervised for this kind of task? I thought tests like this were rather common in Elixir.

I finally figured out what was going on.
Turns out start_supervised is working as expected and is in fact waiting for the GenServer to end handle_continue (well, it is not exactly waiting, it still sends messages and these are put into the queue waiting for the proper time to get executed).

The issue here was the fact that I didn’t do a full cleanup from all the stuff I initiated in my handle_continue. Turns out some of the connections and processes I started remained even after the original GenServer died.

The solution was two-folded:

  • catch all stop signals
  • once a stop signal is detected, do a graceful shutdown

In code, this is translated into:

def init(_) do
    Process.flag(:trap_exit, true) # trap all exits!
    {:ok, %{}, {:continue, :setup_queue}}
end

def handle_continue(:setup_queue, state) do
   # do heavy lifting
    {:noreply, state}
end

def terminate(_reason, state), do:
    # undo heavy lifting

With this and start_supervised inside my setup block, everything works nicely.

That‘s the only way to observe that the init is done, as afaik the GenServer doesn‘t know where it was started from, so cannot actively tell the starting process about it.

1 Like