mariosangiorgio

mariosangiorgio

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?

First Post!

mariosangiorgio

mariosangiorgio

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.

Most Liked

mbuhot

mbuhot

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.

Last Post!

mariosangiorgio

mariosangiorgio

That a good point!

Where Next?

Popular in Questions Top

RisingFromAshes
I’ve read in another post that it may be possible with a router helper - but I couldn’t find an appropriate one, and tbh, I’m still just ...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
hariharasudhan94
Lets say I have map like this fetching from my database %{"_id" =&gt; #BSON.ObjectId&lt;58eb1a7a9ad169198c3dXXXX&gt;, "email" =&gt; ...
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID&lt;0.412.0&gt; terminating ** (Postgrex.Error) FATAL...
New

Other popular topics Top

nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
hariharasudhan94
Lets say I have map like this fetching from my database %{"_id" =&gt; #BSON.ObjectId&lt;58eb1a7a9ad169198c3dXXXX&gt;, "email" =&gt; ...
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New
AstonJ
Posting this to see if we can make things easier for people to get into Neovim. If you use Neovim and have a favourite distro please let ...
New

We're in Beta

About us Mission Statement