Trouble with Mox when testing a GenServer calling a mock in `init/1`

I recently ran into a tricky error while testing a GenServer that uses a mock in its init/1.
Maybe this helps others, or maybe someone has cleaner ideas.

The error looked like this:

** (Mox.UnexpectedCallError) no expectation defined for Supex.Sclang.ScPortMock.open/2
in process #PID<0.206.0> with args [{:spawn, "sclang"}, [:binary]]

At first, I tried the usual:

    test "Sclang GenServer state ok" do
      Supex.Sclang.ScPortMock
      |> expect(:open, fn _name, _opts -> Port.open({:spawn, "cat"}, [:binary]) end)

      assert {:ok, _pid} = start_supervised(Sclang)
      assert %Sclang{} = :sys.get_state(Sclang)
    end

But that failed because the GenServer calls the mock inside init/1.
By the time init/1 runs, the supervised process is in a different pid,
so the expectation defined in the test process didn’t apply.

The missing piece was using Mox.allow/3 with a function that resolves the pid later:

    test "Sclang GenServer state ok" do
      Supex.Sclang.ScPortMock
      |> expect(:open, fn _name, _opts -> Port.open({:spawn, "cat"}, [:binary]) end)

      Supex.Sclang.ScPortMock
      |> allow(self(), fn -> GenServer.whereis(Sclang) end)

      assert {:ok, _pid} = start_supervised(Sclang)
      assert %Sclang{} = :sys.get_state(Sclang)
    end

The key is that allow/3 accepts a function, not just a pid.
When the GenServer invokes the mock in init/1, Mox evaluates the function,
resolves the pid, and lets the call go through.

This finally got rid of the Mox.UnexpectedCallError.

3 Likes

great job figuring it out. You should also be able to set callers in your genserver and Mox will share the mock automatically. Here’s an example Ecto.Adapters.SQL.Sandbox and GenServer restarting with new PID - #3 by martosaur

2 Likes

Thanks! Really elegant solution :slight_smile: