Correctly mocking GenServer child processes

I’ve defined a GenServer like this:

def MyModule do
  use GenServer

  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts)
  end

  def init(opts) do
    {submodule, opts} = Keyword.pop!(opts, :submodule)

    {:ok, pid} = submodule.start_link(opts)

    {:ok, %{submodule: pid}
  end
end

submodule is another Elixir module with a different set of responsibilities, passed as an option. It includes a behaviour definition something like this:

defmodule MySubmodule do
  @callback start_link(any()) :: {:ok, pid()} | {:error, term()}
end

Then the mock is defined as you might expect like this:

Mox.defmock(MySubmoduleMock,
  for: MySubmodule
)

My tests look a bit like this so far:

defmodule MyModuleTest do

  use ExUnit.Case, async: true

  import Mox

  setup do
   
    SubmoduleMock
    |> expect(:start_link, fn opts ->
      {:ok, "fake-pid"}
    end)

    client = start_supervised!({Client, connection: SubmoduleMock})
    {:ok, %{ client: client }}
  end

  test "etc etc", %{ client: client } do
    assert client
  end
end

When I attempt to run the test I get Mox.UnexpectedCallError. The calling process in this case is an instance of MyModule.

I can work around this by enabling global mode. But then that means I have to get rid of async: true and I’d prefer not to if possible.

Since MyModule hasn’t been started, I don’t have a pid to call Mox.allow/3 with.

Is there a way of achieving this without resorting to Mox global mode? Perhaps I’ve arranged my process hierarchy in a mad way… so I’m open to suggestions to modify that too.

Thanks as always for help!

Is there a reason to start the submodule within init/1 of MyModule (a.k.a. already in another process), instead of delegating to submodule in start_link/1 of MyModule? If both processes are indeed meant to run side by side I’d suggest a proper supervision setup.

1 Like

Thanks @LostKobrakai

Yes there is: if MySubmodule crashes then MyModule should too.

But maybe there is a better way of arranging it as you suggest. What I didn’t mention is that MyModule is supervised.

Why the genserver needs to start_link another process? You probably need to start it as part of a supervision tree.
You probably can avoid using mock here and instead play with the functions of ExUnit to write your tests.

I’ve arranged it this way because a crash in MySubmodule should also cause MyModule to crash.

The reason it’s not all defined in the same module is because:

  • MyModule defines a client interface for talking to an external resource
  • MySubmodule defines a transport method (in this case a websocket, but I don’t think that’s pertinent to the question)

The idea of passing the transport module is threefold:

  • A problem in transport layer should cause the client layer to die, restart, recreate connection and so on.
  • It should be possible to pass in a different transport module one day, that doesn’t use websocks for example (although this isn’t in fact required just yet)
  • Most importantly for this question, it should be possible to mock the websocket connection part so that I can test the client independently of transport

That’s what I’m trying to do anyway :wink: !

The right way to do this is with a :one_for_all supervisor. Doing it manually is just a buggier version of the same thing that won’t handle edge cases properly.

4 Likes

Thanks @benwilson512 I’ll give that a go.

(also thanks for the great book!)