Mox and GenServer init

I am working on a Phoenix application that also uses a GenServer to fetch some data from a third party API on startup and periodically to refresh the values it got.

I’ve got everything working but then I wanted to add some tests, using a mock in place of the external service and I’m having some problems. I’m using mox following what is described in the documentation and in the blog post.

My GenServer looks like this:

defmodule MyApp.Collector do
  use GenServer
  @api Application.get_env(:my_app, :third_party_api)

  def start_link() do
    GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
  end

  defp fetch_new_state() do
    @third_party_api.lookup()
    # put the data into an Agent
  end

  defp schedule_work() do
    refresh_interval = 10 * 60 * 1000
    Process.send_after(self(), :refresh, refresh_interval)
  end

  def init(state) do
    schedule_work()
    fetch_new_state()
    {:ok, state}
  end

  def handle_info(:refresh, state) do
    schedule_work()
    fetch_new_state()
    {:noreply, state}
  end

  def handle_info(request, state) do
    super(request, state)
  end
end

The problem I have is that the call to fetch_new_state in the init callback would call the mock before I have a chance to set any expectation: (Mox.UnexpectedCallError) no expectation defined for MyApp.ThirdPartyApi.Mock.lookup/0 in process #PID<0.293.0>

I tried to add expectations as early as possible in test_helper.ex, but that didn’t help because mix tries to start all the processes in the supervision tree as part of the mix task, which I believe always happen before any call to test_helper.ex.

I could use mix test --no-start to prevent mix from starting the supervision tree, but that feels wrong because then I would need to manually start all the applications in the correct order.

Another possible workaround is to just schedule some work to be done in init, without calling the api but I’m not very convinced by this approach either.

Is it fine to do some work in the init callback of a GenServer? If it is, how do you handle a scenario like the one I described above?

I found some articles (e.g. this one) saying that it’s better to avoid doing expensive operation on init and instead do something like:

defmodule MyServer do
  use GenServer

  def init([]) do
    {:ok, [], 0}
  end

  def handle_info(:timeout, _state) do
    state = something_slow()

    {:noreply, state}
  end
end

or something like this:

defmodule MyServer do
  use GenServer

  def init([]) do
    send self(), :my_fancy_setup

    {:ok, []}
  end

  def handle_info(:my_fancy_setup, _state) do
    state = something_slow()

    {:noreply, state}
  end
end

or even to just call the function that provides the initial state from outside the GenServer and pass it in GenServer.start_link.

Regarding mox, I am convinced that I was trying to use it wrong. First of all, the test bring up all the applications before calling test_helper.ex so setting expectations there is hopeless. Moreover, by default expectations want to be tied to a process and I would need to set the global mode, which I shouldn’t need.

I think that what I really want is to test in isolation my MyApp.Collector state refreshing (and I can do that creating a fresh instance of the GenServer) while disabling it completely when I want to test that my controllers are able to query it properly, in that case I should be able to use mox without problems.

The only thing that this wouldn’t support are async test cases, but from the module documentation autogenerated by the Phoenix template it seems that it’s already off for everything that has to interact with the database:

Finally, if the test case interacts with the database, it cannot be async. For this reason, every test runs inside a transaction which is reset at the beginning of the test unless the test case is marked as async.

unless the test case is marked as async.

You can certainly run async tests against the database using the Ecto 2 sandbox.

It’s just not a safe default for Phoenix to run tests async since you may be interacting with stateful named processes.

1 Like
defmodule MyServer do
  use GenServer

  def init([]) do
    {:ok, [], 0}
  end

  def handle_info(:timeout, _state) do
    state = something_slow()

    {:noreply, state}
  end
end

handle_info(:timeout, _state) would also be called if you return {:reply, reply, state, timeout} or {:noreply, state, timeout} from other handlers and then until you get another message and then again given you still return timeout in the reply/noreply tuples, so it’s a rather dangerous “idiom”. send(self(), :my_fancy_setup) seems safer to me.

That a good point!