dashbitco / mox

Mocks and explicit contracts in Elixir
1.35k stars 77 forks source link

Expectations not visible to stop_supervised regardless of allowances or global mode #84

Closed sgilson closed 5 years ago

sgilson commented 5 years ago

I came across an incompatibility with ExUnit.Callbacks.start_supervised/2 and stop_supervised/1 in which expectations are not shared with the supervised process while executing the teardown logic of a GenServer. I was testing a GenServer that must (attempt) to send an HTTP request during shutdown and all mocks outside of the teardown logic worked exactly as expected. Here is a minimal program that can reproduce the issue.

defmodule TeardownGenServer do
  use GenServer

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

  @impl true
  def init(_) do
    Process.flag(:trap_exit, true)
    MockClient.start()
    {:ok, nil}
  end

  @impl true
  def terminate(_, _) do
    MockClient.stop()
  end
end

defmodule MinimalClient do
  @callback start :: :ok
  @callback stop :: :ok
end

defmodule MoxTest do
  use ExUnit.Case

  import Mox

  setup :set_mox_global
  setup :verify_on_exit!

  setup_all do
    defmock(MockClient, for: MinimalClient)
    :ok
  end

  test "mocks are called" do
    MockClient
    |> expect(:start, fn -> :ok end)
    |> expect(:stop, fn -> :ok end)

    # expectation for start/0 is fulfilled
    {:ok, pid} = start_supervised(TeardownGenServer)
    IO.puts("GenServer has pid: #{inspect(pid)}")
    # expectation for stop/0 not defined...?
    stop_supervised(pid)
  end
end

This will lead to a curious result:

GenServer has pid: #PID<0.368.0>
11:06:47.377 [error] GenServer #PID<0.368.0> terminating
** (Mox.UnexpectedCallError) no expectation defined for MockClient.stop/0 in process #PID<0.368.0> with args []
    (mox) lib/mox.ex:599: Mox.__dispatch__/4
    (stdlib) gen_server.erl:673: :gen_server.try_terminate/3
    (stdlib) gen_server.erl:858: :gen_server.terminate/10
    (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3
Last message: {:EXIT, #PID<0.367.0>, :shutdown}
State: nil

The TeardownGenServer had access to the start/0 mock, but not stop/0 even though it is the same process. I attempted to remedy this issue with many combinations of allowances and expectation placement, but I could simply not get Mox to recognize the expectations within stop_supervised without resorting to some nasty global mocks that I won't share here.

Workaround: use GenServer methods to start and stop the server under test and avoid the ExUnit callbacks when managing processes.

josevalim commented 5 years ago

That's because the start_supervised! processes are terminated only after the test process is done. By then, the process that defines the mocks is done, which means the mocks are no longer accessible. Your workaround is correct in this case.

sgilson commented 5 years ago

Makes perfect sense. Is this behavior also expected when stop_supervised is explicitly called inside the test? I would think that the test process would still be alive, but this does not seem to be the case...?

josevalim commented 5 years ago

No, not really. So my understanding is actually wrong. I will have to investigate this then. Thanks for the report.

josevalim commented 5 years ago

So the error is that you were actually not stopping the GenServer. It was returning {:error, :not_found}. You must stop it using its ID, which by default is the module name:

stop_supervised(TeardownGenServer)