How to test this GenServer function?

I have the following client function in a GenServer and want to unit test it without luck.

defmodule Manager do
  use GenServer

  def update_step(execution_id, step_name, status) do
    pid = case GenServer.whereis(execution_id) do
      nil ->
        {:ok, pid} = GenServer.start_link(__MODULE__, execution_id, name: execution_id)
        pid

      pid ->
        pid
    end

    GenServer.cast(pid, {:update_step, step_name, status})
  end

  def init(args) do
    {:ok, {}}
  end

  def handle_cast(message, state) do
  end 
end

Ideally I would like test that if a Manager process exists then its pid is used pased to cast, otherwise a new Manager process is started before sending a cast call to it.

How do I test this function?

1 Like

What are you trying to achieve?

You should be testing the side effects of any given interaction. Whether or not an existing GenServer existed or not is an implementation detail, imo.

It also seems like you’re re-inventing the concept of a Supervisor.

I’d have the GenServer fire up on app start with its name being the module. Then you can cast to it directly:

GenServer.cast(Module.Name, :message)

If you want to start the genserver on demand, look into a DynamicSupervisor

1 Like

You can simply assert for the existence of a new pid after calling the genserver.

refute Process.whereis(:new_pid)
Manager.update_step(:new_pid, "step", "status")
assert Process.whereis(:new_pid)

Another idea would be to start your GenServer under a Supervisor (using Supervisor.start_child) and then do assertions on Supervisor.count_children

1 Like

The code I provided is contrived because the question is about testing this function to assert it will either cast to an existing process or start one before calling cast to it. I do come from a Ruby background and this desire may be due to how easy it is to do that in Ruby.

There will be many GenServers which have to be started again after an app restart event. These GenServers supervise the progression of a series of Oban jobs (which may already be enqueued). Those Oban jobs send messages to the GenServers to report progress. This design allows for recovery from an app restart. In other words GenServers can be started on demand by another process after a restart event such as a new deployment.

Given the behaviour I described, I don’t agree this is mere implementation detail and deserves no testing because if it breaks then the guarantees we rely on will break.

It’s easier to let Oban call update_step to only restart GenServers which must exist. I do plan to start them under a DynamicSupervisor but that detail seems unrelated to the question of how to test this function in isolation.

I think this makes sense. I’m curious how do you test that update_state will issue a cast call also?

I usually trace messages with :erlang.trace(pid, true, [:receive]) to make my test receive the tested process messages, and then perform assert_receive assertions. But it’s a bit of chicken & egg issue, since you need to call trace/3 at the very beginning of your test and don’t have any pid yet.

I assume your cast call will issue a state change, if so you can use :sys.get_state/1 to check the process state has been updated as it should be.

You could also register your test process as execution_id and check it receives a cast message.