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