@kokolegorille I decided to take a crack at a generally useful mock GenServer, code below. It’s not perfect, but seems useful given the time spent 
I’d use it like this:
setup do
{:ok, manager_pid} = Mock.GenServer.start_link
%{
manager_pid: manager_pid,
}
end
test "create success returns valid results, exits normally", %{manager_pid: manager_pid} do
{:ok, pid} = create_and_execute_worker(manager_pid)
ref = Process.monitor(pid)
receive do
{:DOWN, ^ref, :process, same_pid, :normal} ->
assert pid == same_pid
[response] = Mock.GenServer.get_call_values(manager_pid)
assert {:create_success, %{result: {:ok, _metadata, _ip, _stats}}} = response
after
1000 ->
raise "timeout"
end
end
Server code:
defmodule Mock.GenServer do
@moduledoc """
Documentation for Mock.GenServer.
Can be used to mock communication to/from a GenServer. Stores all data
passed to call/cast/info for later examination.
Responses to GenServer.call can be configured by mapping the call 'action'
to the desired response. The action is derived as follows.
- If data is an atom, the action is the atom.
- If data is a tuple, the action is the first element of the tuple.
e.g, if another server issues GenServer.call({:create, data}), the action
would be :create.
The response map is passed when creating the server, keys are actions,
values are the desired response for the action.
Caveats:
- GenServer.call data cannot exactly match any of the internal handle_call
patterns (:call_values, :cast_values, etc).
- An action response can only be derived if the passed data is an atom or
a tuple. This seems reasonable given standard GenServer usage. If no
action can be determined from the response map, a default response is
returned.
"""
use GenServer
require Logger
@logger_metadata [name: :mock_genserver]
@default_response :ok
## API
def start_link(response_map \\ %{}) do
Logger.debug fn -> {"Starting mock GenServer instance, with response map: " <> inspect(response_map), @logger_metadata} end
GenServer.start_link(__MODULE__, response_map)
end
def get_call_values(pid) do
GenServer.call(pid, :call_values)
end
def get_cast_values(pid) do
GenServer.call(pid, :cast_values)
end
def get_info_values(pid) do
GenServer.call(pid, :info_values)
end
def update_response(pid, action, response) do
GenServer.call(pid, {:update_response, action, response})
end
## Callbacks
def init(response_map) do
Logger.debug fn -> {"Initializing mock GenServer #{inspect(self())} instance with response map: " <> inspect(response_map), @logger_metadata} end
state = %{
response_map: response_map,
call_values: [],
cast_values: [],
info_values: [],
}
{:ok, state}
end
def handle_call(:call_values, _from, state) do
{:reply, state.call_values, state}
end
def handle_call(:cast_values, _from, state) do
{:reply, state.cast_values, state}
end
def handle_call(:info_values, _from, state) do
{:reply, state.info_values, state}
end
def handle_call({:update_response, action, response}, _from, state) do
state = put_in(state[:response_map][action], response)
{:reply, state.response_map, state}
end
def handle_call(term, _from, state) do
response = Map.get(state.response_map, get_action(term)) || @default_response
{:reply, response, Map.put(state, :call_values, state.call_values ++ [term])}
end
def handle_cast(term, state) do
{:noreply, Map.put(state, :cast_values, state.cast_values ++ [term])}
end
def handle_info(term, state) do
{:noreply, Map.put(state, :info_values, state.info_values ++ [term])}
end
def terminate(reason, state) do
Logger.debug fn -> {"Terminating mock GenServer #{inspect(self())} instance, reason: #{inspect(reason)}, state: #{inspect(state)}", @logger_metadata} end
:ok
end
defp get_action(term) do
if is_atom(term), do: term, else: elem(term, 0)
end
end