Fl4m3Ph03n1x

Fl4m3Ph03n1x

Background

I have a small app where I have a pool that creates several workers. The pool acts as a supervisor for said workers.

Both (the pool and the workers) have a dependency on another module, the ExRegistry. This module registers any process that provides a key.

Test

While using Mox I want to make sure the worker is doing its job correctly, I don’t really want to test the registry just yet. So I stub the RegistryMock with the real one:

stub(RegistryMock, :via_tuple, &ExRegistry.via_tuple/1)
{:ok, _pid} = Worker.start_link({1})

Problem

The problem here is that the pool fails to start:

 Could not start application mox_issue: MoxIssue.Application.start(:normal, []) returned an error: shutdown: failed to start child: MoxIssue.Pool
    ** (EXIT) an exception was raised:
        ** (Mox.UnexpectedCallError) no expectation defined for MoxIssue.RegistryMock.via_tuple/1 in process #PID<0.163.0>

I think this happens because the pool is another process, which also needs access to the RegistryMock but Mox fails to give it access. To test this I set the options on Mox for global so all processes can use the mocks, but it still fails with the same error.

Question

How does Mox behave with multiple processes requiring the same Mock?

Showing Posts 1 to 10

peerreynders

peerreynders

A quick scan of the source code suggests to me that expectations are registered by process id - if that is true the same expectation would have to registered by/for the pool process.

Fl4m3Ph03n1x

Fl4m3Ph03n1x OP

So, how would I make this test pass? It would be impossible unless I override the global_owner_pid and that’s impossible/not a good idea.

Perhaps my approach to testing this concept is wrong? How would you do it?

jeremyjh

jeremyjh

Mox supports a global mode.

peerreynders

peerreynders

This approach seems to work (when setup :set_mox_global doesn’t):

defmodule MockShare do
  import Mox

  def init(args) do
    MyApp.CalcMock
    |> expect(:add, fn x, y -> x + y end)
    |> expect(:mult, fn x, y -> x * y end)

    {:ok, args}
  end

  def handle_call({:share, pid}, _from, state) do
    Mox.allow(MyApp.CalcMock, self(), pid)
    {:reply, :ok, state}
  end

  # ---

  def child_spec(_args),
    do: %{
      id: __MODULE__,
      start: {__MODULE__, :start_link, []},
      restart: :permanent,
      shutdown: 5000,
      type: :worker
    }

  def start_link(),
    do: GenServer.start_link(__MODULE__, [])

  def share(share_id, pid),
    do: GenServer.call(share_id, {:share, pid})
end

defmodule MyAppTest do
  use ExUnit.Case, async: true

  import Mox

  def share_mock(_context) do
    {:ok, share_pid} = start_supervised(MockShare)

    [share_pid: share_pid]
  end

  setup_all :share_mock
  setup :verify_on_exit!

  test "Use add expectation in task", context do
    MockShare.share(context.share_pid, self())

    task = Task.async(fn -> MyApp.CalcMock.add(2, 3) end)
    result = Task.await(task)
    assert result == 5
  end

  test "Use mult expectation in task", context do
    MockShare.share(context.share_pid, self())

    task = Task.async(fn -> MyApp.CalcMock.mult(2, 3) end)
    result = Task.await(task)
    assert result == 6
  end
end

Mox.allow/3


The only way I could get global mode to work:

defmodule MyAppTest do
  use ExUnit.Case

  import Mox

  # setup :set_mox_global - doesn't work

  setup_all do
    expect(MyApp.CalcMock, :add, fn x, y -> x + y end)
    expect(MyApp.CalcMock, :mult, fn x, y -> x * y end)
    set_mox_global()
    %{}
  end

  test "Use add expectation in task" do
    task = Task.async(fn -> MyApp.CalcMock.add(2, 3) end)
    result = Task.await(task)
    assert result == 5
  end

  test "Use mult expectation in task" do
    task = Task.async(fn -> MyApp.CalcMock.mult(2, 3) end)
    result = Task.await(task)
    assert result == 6
  end
end

The way I understand the documentation this should only work in global mode …

defmodule MyAppTest do
  use ExUnit.Case, async: true

  import Mox

  setup [:set_mox_private, :verify_on_exit!]

  def add,
    do: MyApp.CalcMock.add(2, 3)

  def mult,
    do: MyApp.CalcMock.mult(2, 3)

  test "Use add expectation in task" do
    expect(MyApp.CalcMock, :add, fn x, y -> x + y end)
    task = Task.async(&add/0)
    result = Task.await(task)
    assert result == 5
  end

  test "Use mult expectation in task" do
    expect(MyApp.CalcMock, :mult, fn x, y -> x * y end)
    task = Task.async(&mult/0)
    result = Task.await(task)
    assert result == 6
  end
end

and yet the task process has no problem using the mocked functions.

al2o3cr

al2o3cr

There’s a note about this below the example - on Elixir 1.8+ Mox can use $callers to detect when a parent process has an expectation set. (see also the similar machinery in DBConnection)

That’s not going to help for your case, though, since the test process and the registry aren’t related in a parent->child sense.

peerreynders

peerreynders

It seems in global mode the expectations have to come from a single process:

defmodule Mocker do
  import Mox

  def init(args) do
    set_mox_global()

    MyApp.CalcMock
    |> expect(:add, fn x, y -> x + y end)
    |> expect(:mult, fn x, y -> x * y end)

    {:ok, args}
  end

  # ---

  def child_spec(_args),
    do: %{
      id: __MODULE__,
      start: {__MODULE__, :start_link, []},
      restart: :permanent,
      shutdown: 5000,
      type: :worker
    }

  def start_link(),
    do: GenServer.start_link(__MODULE__, [])
end

defmodule Adder do
  def init(args),
    do: {:ok, args}

  def handle_call({:add, term1, term2}, _from, state) do
    sum = MyApp.CalcMock.add(term1, term2)
    {:reply, {:ok, sum}, state}
  end

  # ---

  def child_spec(_args),
    do: %{
      id: __MODULE__,
      start: {__MODULE__, :start_link, []},
      restart: :permanent,
      shutdown: 5000,
      type: :worker
    }

  def start_link(),
    do: GenServer.start_link(__MODULE__, [])

  def add(adder_id, arg1, arg2),
    do: GenServer.call(adder_id, {:add, arg1, arg2})
end

defmodule Multiplier do
  def init(args),
    do: {:ok, args}

  def handle_call({:multiply, factor1, factor2}, _from, state) do
    product = MyApp.CalcMock.mult(factor1, factor2)
    {:reply, {:ok, product}, state}
  end

  # ---

  def child_spec(_args),
    do: %{
      id: __MODULE__,
      start: {__MODULE__, :start_link, []},
      restart: :permanent,
      shutdown: 5000,
      type: :worker
    }

  def start_link(),
    do: GenServer.start_link(__MODULE__, [])

  def multiply(multiplier_id, arg1, arg2),
    do: GenServer.call(multiplier_id, {:multiply, arg1, arg2})
end

defmodule MyAppTest do
  use ExUnit.Case

  setup_all do
    {:ok, _mocker_id} = start_supervised(Mocker)
    {:ok, adder_id} = start_supervised(Adder)
    {:ok, multiplier_id} = start_supervised(Multiplier)

    %{
      adder_id: adder_id,
      multiplier_id: multiplier_id
    }
  end

  test "Use add expectation", context do
    assert Adder.add(context.adder_id, 2, 3) == {:ok, 5}
  end

  test "Use mult expectation", context do
    assert Multiplier.multiply(context.multiplier_id, 2, 3) == {:ok, 6}
  end
end

Managing multiple sets of mocks with explicit allowances is a bit more work:

defmodule MockShare do
  import Mox

  def init(args),
    do: {:ok, args}

  def handle_call({:allow, module, pid}, _from, state) do
    Mox.allow(module, self(), pid)
    {:reply, :ok, state}
  end

  def handle_call({:expect, expect_fun}, _from, state) do
    expect_fun.()
    {:reply, :ok, state}
  end

  # ---

  def child_spec(_args) do
    %{
      id: __MODULE__,
      start: {__MODULE__, :start_link, []},
      restart: :permanent,
      shutdown: 5000,
      type: :worker
    }
  end

  def start_link(),
    do: GenServer.start_link(__MODULE__, [])

  def allow(share_id, module, pid),
    do: GenServer.call(share_id, {:allow, module, pid})

  def expect(share_id, expect_fun),
    do: GenServer.call(share_id, {:expect, expect_fun})
end

defmodule Adder do
  def init(args),
    do: {:ok, args}

  def handle_call({:add, term1, term2}, _from, state) do
    sum = MyApp.CalcMock.add(term1, term2)
    {:reply, {:ok, sum}, state}
  end

  # ---

  def child_spec(_args),
    do: %{
      id: __MODULE__,
      start: {__MODULE__, :start_link, []},
      restart: :permanent,
      shutdown: 5000,
      type: :worker
    }

  def start_link(),
    do: GenServer.start_link(__MODULE__, [])

  def add(adder_id, arg1, arg2),
    do: GenServer.call(adder_id, {:add, arg1, arg2})
end

defmodule Multiplier do
  def init(args),
    do: {:ok, args}

  def handle_call({:multiply, factor1, factor2}, _from, state) do
    product = MyApp.CalcMock.mult(factor1, factor2)
    {:reply, {:ok, product}, state}
  end

  # ---

  def child_spec(_args),
    do: %{
      id: __MODULE__,
      start: {__MODULE__, :start_link, []},
      restart: :permanent,
      shutdown: 5000,
      type: :worker
    }

  def start_link(),
    do: GenServer.start_link(__MODULE__, [])

  def multiply(multiplier_id, arg1, arg2),
    do: GenServer.call(multiplier_id, {:multiply, arg1, arg2})
end

defmodule MyAppTest do
  use ExUnit.Case

  import Mox, only: [expect: 3]

  setup_all do
    {:ok, add_share_id} = start_supervised(MockShare, id: :add_share)

    MockShare.expect(
      add_share_id,
      fn ->
        expect(MyApp.CalcMock, :add, fn x, y -> x + y end)
      end
    )

    {:ok, mult_share_id} = start_supervised(MockShare, id: :mult_share)

    MockShare.expect(
      mult_share_id,
      fn ->
        expect(MyApp.CalcMock, :mult, fn x, y -> x * y end)
      end
    )

    {:ok, adder_id} = start_supervised(Adder)
    MockShare.allow(add_share_id, MyApp.CalcMock, adder_id)

    {:ok, multiplier_id} = start_supervised(Multiplier)
    MockShare.allow(mult_share_id, MyApp.CalcMock, multiplier_id)

    %{
      adder_id: adder_id,
      multiplier_id: multiplier_id
    }
  end

  test "Use add expectation", context do
    assert Adder.add(context.adder_id, 2, 3) == {:ok, 5}
  end

  test "Use mult expectation", context do
    assert Multiplier.multiply(context.multiplier_id, 2, 3) == {:ok, 6}
  end
end

Getting setup :set_mox_global to work:

defmodule Adder do
  def init(args),
    do: {:ok, args}

  def handle_call({:add, term1, term2}, _from, state) do
    sum = MyApp.CalcMock.add(term1, term2)
    {:reply, {:ok, sum}, state}
  end

  # ---

  def child_spec(_args),
    do: %{
      id: __MODULE__,
      start: {__MODULE__, :start_link, []},
      restart: :permanent,
      shutdown: 5000,
      type: :worker
    }

  def start_link(),
    do: GenServer.start_link(__MODULE__, [])

  def add(adder_id, arg1, arg2),
    do: GenServer.call(adder_id, {:add, arg1, arg2})
end

defmodule Multiplier do
  def init(args),
    do: {:ok, args}

  def handle_call({:multiply, factor1, factor2}, _from, state) do
    product = MyApp.CalcMock.mult(factor1, factor2)
    {:reply, {:ok, product}, state}
  end

  # ---

  def child_spec(_args),
    do: %{
      id: __MODULE__,
      start: {__MODULE__, :start_link, []},
      restart: :permanent,
      shutdown: 5000,
      type: :worker
    }

  def start_link(),
    do: GenServer.start_link(__MODULE__, [])

  def multiply(multiplier_id, arg1, arg2),
    do: GenServer.call(multiplier_id, {:multiply, arg1, arg2})
end

defmodule MyAppTest do
  use ExUnit.Case

  import Mox

  setup_all do
    {:ok, adder_id} = start_supervised(Adder)
    {:ok, multiplier_id} = start_supervised(Multiplier)

    %{
      adder_id: adder_id,
      multiplier_id: multiplier_id
    }
  end

  setup :set_mox_global

  test "Use add expectation", context do
    expect(MyApp.CalcMock, :add, fn x, y -> x + y end)
    assert Adder.add(context.adder_id, 2, 3) == {:ok, 5}
  end

  test "Use mult expectation", context do
    expect(MyApp.CalcMock, :mult, fn x, y -> x * y end)
    assert Multiplier.multiply(context.multiplier_id, 2, 3) == {:ok, 6}
  end
end

Seems what is happening is that setup :set_mox_global makes the individual test the global owner for the duration of the test - consequently that testing process would have to register all the expectations that are to be shared for the scope of that particular test.

Fl4m3Ph03n1x

Fl4m3Ph03n1x OP

Been there, done that :stuck_out_tongue:

@peerreynders That’s quite a lot to go through, I am still digesting it!

Fl4m3Ph03n1x

Fl4m3Ph03n1x OP

@peerreynders After reading all of your replies, I do feel like it’s totally overkill to define a GenServer module for each function of the interface I want to test.

Surely, @josevalim had a better idea in mind when he created Mox, I am fairly certain this is not the way Mox should be used and that I am missing something here.

Your replies have been invaluable though, I have learned a lot!
I will however look into other solutions for this issue and if I fail, I guess I will simply ditch Mox, as I don’t think the library is worth all the extra complexity I am paying - unless I am the only one here who thinks that creating a GenServer for each function of each interface is overkill, in which case I will just politely disagree :smiley:

hubertlepicki

hubertlepicki

I don’t know if you should be using it like that in first place. You are attempting to mock the piece of infrastructure you are in control, and mocking/stubbing in my mind should be reserved to dealing with elements of infrastructure you don’t have control over: like remote services/endpoints, systems talking to external systems etc.

If you don’t want to touch ExRegistry in your tests, you could write a testing module that does something similar that makes sense in your test and inject it in it’s place for testing purposes, or isolate the worker further and test it in isolation.

peerreynders

peerreynders

My sense is that a typical use case is something like:

defmodule MyAppTest do
  use ExUnit.Case

  import Mox

  setup :set_mox_global

  test "Use add expectation" do
    {:ok, adder_id} = start_supervised(Adder)

    expect(MyApp.CalcMock, :add, fn x, y -> x + y end)
    assert Adder.add(adder_id, 2, 3) == {:ok, 5}

    stop_supervised(adder_id)
  end

  test "Use mult expectation" do
    {:ok, multiplier_id} = start_supervised(Multiplier)

    expect(MyApp.CalcMock, :mult, fn x, y -> x * y end)
    assert Multiplier.multiply(multiplier_id, 2, 3) == {:ok, 6}

    stop_supervised(multiplier_id)
  end
end

where Adder and Multiplier are pieces of the SUT - setting up just enough environment to support the test.

I don’t think it was meant to be used with a whole and running SUT.

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
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
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
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New
ryanwinchester
apply_graft/2 doesn’t rewrite an add_many sub-workflow’s deps on an add step. Grafted jobs cancel with “upstream job was deleted” Version...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
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

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews