mariosangiorgio

mariosangiorgio

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?

Showing Posts 1 to 4

mariosangiorgio

mariosangiorgio OP

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.

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.

idi527

idi527

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.

mariosangiorgio

mariosangiorgio OP

That a good point!

— All posts loaded —

Where Next? Top

Trending in Questions Top

Blokh
Hey guys, I’ve got a huge CSV ( around 10 GB ) that needs to be processed hourly Do you guys have any suggestions what is the best prac...
New
kszambelanczyk
Hello! Could someone please give me a help/sample code, how to delete a file from s3 using waffle/waffle_ecto from Phoenix app. I creat...
New
Onor.io
I have what I’ve heard referred to as a “lookup table” in my database. This is a way of assigning codes to common values. One common lo...
New
jaybe78
Hello, I’m developing a online persistent chat system (what’s app) like using elixir/dynamodb/aws for a mobile app(flutter). The diffic...
New
Trolleger
What approach to take when sending live updates to “random” users Hi! I have a question, I have a little chat app, and when I create a DM...
New
matt-savvy
Anyone here using Honeybadger? My Honeybadger account is being overwhelmed with noise from some bots. Seeing a lot of Bandit.HTTPError...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New

Other Trending Topics Top

CodeSync
Hologram: The Journey to Local-First Elixir in the Browser - Bart Blast | ElixirConf EU 2026 https://www.youtube.com/watch?v=qqpNovT7cys...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
wintermeyer
There are three potential reasons for members of this forum to have a look at https://vutuv.de You are tired or annoyed of LinkedIn. Yo...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews