Background
I have a small app with a pool manager, a couple workers and a registry.
For the sake of learning Mox, I made a small test that uses Mox, but it fails for a reason I can’t understand.
Code
This code is a MWE app that starts a registry and a pool:
defmodule MoxIssue.Application do
use Application
def start(_type, _args) do
Supervisor.start_link(
[
MoxIssue.ExRegistry,
MoxIssue.Pool
],
strategy: :one_for_one
)
end
end
The pool creates 3 workers and acts as a supervisor:
defmodule MoxIssue.Pool do
alias MoxIssue.Worker
require Logger
@registry Application.fetch_env!(:mox_issue, :registry)
def start_link do
children = Enum.map(1..3, &worker_spec/1)
Logger.debug("Starting supervisor")
res = Supervisor.start_link(
children,
strategy: :one_for_one,
name: @registry.via_tuple({__MODULE__, __MODULE__})
)
Logger.debug("Supervisor started")
res
end
defp worker_spec(worker_id), do:
Supervisor.child_spec({Worker, {worker_id}}, id: {worker_id})
def child_spec(_) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, []},
type: :supervisor
}
end
end
Each Worker is a dummy echo worker that saves a list of messages echoed:
defmodule MoxIssue.Worker do
use GenServer
require Logger
@registry Application.fetch_env!(:mox_issue, :registry)
defmodule State do
defstruct messages: [], worker_id: nil
end
def start_link({worker_id}) do
Logger.debug("Starting worker #{inspect worker_id}")
res = GenServer.start_link(
__MODULE__,
%State{worker_id: worker_id},
name: @registry.via_tuple({__MODULE__, worker_id})
)
Logger.debug("Worker registers OK")
res
end
def echo(msg, id) do
[{pid, _val}] = @registry.lookup({__MODULE__, id})
GenServer.call(pid, {:echo, msg})
end
def init(state), do: {:ok, state}
def handle_call({:echo, msg}, _from, state) do
Logger.info(msg)
new_state = %State{state | messages: [msg] ++ state.messages}
{:reply, :ok, new_state}
end
end
Nothing extraordinary here. The most complex piece of code is the Registry, which I took from Elixir in Action and slightly modified for this example. Because Mox really wants you to have contracts, here is the contract used and it’s implementation:
defmodule MoxIssue.ProcessRegistry do
@type via_tuple_type ::
{:via, module, {module, {module, any}}} |
{:via, module, {module, {module, any}, any}}
@type module_key :: {module, any}
@callback lookup(module_key) :: [{pid, any}]
@callback via_tuple(module_key) :: via_tuple_type
@callback via_tuple(module_key, any) :: via_tuple_type
end
defmodule MoxIssue.ExRegistry do
@behaviour MoxIssue.ProcessRegistry
alias MoxIssue.ProcessRegistry
########
# API #
########
@impl ProcessRegistry
def lookup({_module, _id} = key), do: Registry.lookup(__MODULE__, key)
@impl ProcessRegistry
def via_tuple({_module, _id} = key), do:
{:via, Registry, {__MODULE__, key}}
@impl ProcessRegistry
def via_tuple({_module, _id} = key, value), do:
{:via, Registry, {__MODULE__, key, value}}
@spec start_link() :: {:ok, pid} | {:error, any}
def start_link, do:
Registry.start_link(keys: :unique, name: __MODULE__)
#############
# Callbacks #
#############
@spec child_spec(any) :: Supervisor.child_spec
def child_spec(_) do
Supervisor.child_spec(
Registry,
id: __MODULE__,
start: {__MODULE__, :start_link, []}
)
end
end
Test
The test is rather useless. I am trying to replicate an issue with another app we have, so this is the smallest working example I could come up with:
defmodule MoxIssueTest do
use ExUnit.Case
import Mox
alias MoxIssue.RegistryMock
alias MoxIssue.ExRegistry
alias MoxIssue.Worker
# Make sure mocks are verified when the test exits
setup :verify_on_exit!
describe "create/1" do
test "returns an application ready to use with default values" do
stub_with(RegistryMock, ExRegistry)
{:ok, _pid} = Worker.start_link({1})
end
end
end
This test fails with:
no expectation defined for MoxIssue.RegistryMock.via_tuple/1 in process #PID<0.163.0>
Which I don’t understand. The whole reason for using stub_with
is exactly because I don’t want to expect anything, nor do i want to assert that function X was called Y times. I just want to replace the Mock with an implementation and have it run.
Why am I getting this error?