Mox fails test that should pass

Background

I am trying to use Mox to replicate some simple interactions but I am failing test that should be passing (or so I think):

Code

After creating a basic hello world project I modified to the following:


defmodule Testmox do
  alias MyApp.Calculator.Impl, as: Calc

  def hello, do: "Hello #{inspect Calc.add(1, 1)}"
  def bye, do: "Bye #{inspect Calc.mult(2, 2)}"
end

Textmox uses a basic calculator module:

defmodule MyApp.Calculator do
  @callback add(integer(), integer()) :: integer()
  @callback mult(integer(), integer()) :: integer()
end

defmodule MyApp.Calculator.Impl do
  @behaviour MyApp.Calculator

  alias MyApp.Calculator

  @impl Calculator
  def add(x, y), do: x + y

  @impl Calculator
  def mult(x, y), do: x * y
end

As you see, nothing extraordinary here. Now onto the test!

defmodule CalculatorTest do
  use ExUnit.Case, async: true

  alias Testmox
  alias MyApp.Calculator.Impl, as: Calc

  import Mox

  # Make sure mocks are verified when the test exits
  setup :set_mox_global
  setup :verify_on_exit!

  test "invokes add and mult" do
    MyApp.CalcMock
    |> expect(:add, &Calc.add/2)
    |> expect(:mult, &Calc.mult/2)

    Testmox.hello()
    Testmox.bye()
  end
end

Problem?

This test fails, even though Calc.add and Calc.mult are being called in the hello and bye functions respectively. I have even set the mode to global, so other processes can use it, but it still fails the tests. What am I doing wrong?

EDIT: I have also changed test_helpers.ex:

ExUnit.start()
Mox.defmock(MyApp.CalcMock, for: MyApp.Calculator)

Testmox.hello calls MyApp.Calculator.Impl.add not MyApp.CalcMock.add. In elixir there nothing, which would allow a library to change code, so Mox cannot dynamically alter what get’s called, so you need to find a way to make the implementation changeable on your own. The options for that are using the app env to configure the implementation, passing the implementation as argument to the function or some compile time magic if you don’t need to change the implementation at runtime.

The other problem is that you’re running the tests before running defmock in your test_helpers.exs.

1 Like

So, this should work, right?

defmodule CalculatorTest do
  use ExUnit.Case, async: true

  alias Testmox
  alias MyApp.Calculator.Impl, as: Calc

  import Mox

  # Make sure mocks are verified when the test exits
  setup :set_mox_global
  setup :verify_on_exit!

  test "invokes add and mult" do
    Mox.defmock(Calc, for: MyApp.Calculator)

    expect(Calc, :add, &Calc.add/2)

    Testmox.hello()
  end
end

It doesn’t…

1 Like

I still don’t see any code to make Testmox.hello call the module defined by Mox instead of calling the hardcoded implementation of MyApp.Calculator.Impl.

1 Like

Both Testmox.hello and the test now use the same implementation, the same module, both use MyApp.Calculator.Impl.

The following test still blows up:

defmodule CalculatorTest do
  use ExUnit.Case, async: true

  alias Testmox
  alias MyApp.Calculator.Impl, as: Calc

  import Mox

  # Make sure mocks are verified when the test exits
  setup :verify_on_exit!

  test "invokes add and mult" do
    Mox.defmock(Calc, for: MyApp.Calculator)

    expect(Calc, :add, 1, &Calc.add/2)

    actual = Testmox.hello()
    assert actual === "Hello 2"
  end
end

But its because MyApp.Calculator.Impl.add/2 is being called twice instead of just once. This truly blows my mind…

I think you don’t really get how Mox works yet. It doesn’t matter what you use as callback in expect() for the issues you’re having. Testmox.hello is hardcoded to call MyApp.Calculator.Impl.add, which means it’s never going to not call your actual implementation, which you probably don’t want to happen as you’re trying to mock it.

What Mox.defmock(Calc, for: MyApp.Calculator) is doing is creating a new module with the name you supply as the first argument. Mox can then assert if the functions on this new module are actually called and let it return the values by the callbacks you supply via expect. It does not somehow magically instrument existing implementations to do different things than before.

You last implemenation is does basically override your existing module with a new module, which is not what you want to do.

1 Like

Ok then, how would you fix this?

Give defmock a name, which is not already used by another module. Decide on a way to let Textmox.hello call functions on that new module instead of MyApp.Calculator.Impl.

So I suppose that I do need to create a module called MyApp.CalcMock that is full of empty functions for Mox to use?

defmodule MyApp.CalcMock do
  @behaviour MyApp.Calculator

  alias MyApp.Calculator

  @impl Calculator
  def add(_x, _y), do: 0

  @impl Calculator
  def mult(_x, _y), do: 0
end

Test:

defmodule CalculatorTest do
  use ExUnit.Case, async: true

  alias Testmox
  alias MyApp.Calculator.Impl, as: Calc

  import Mox

  # Make sure mocks are verified when the test exits
  setup :verify_on_exit!

  test "invokes add and mult" do
    Mox.defmock(MyApp.CalcMock, for: MyApp.Calculator)

    expect(MyApp.CalcMock, :add, 1, &Calc.add/2)

    actual = Testmox.hello()
    assert actual === "Hello 2"
  end
end

And then I have to dinamicaly check which module to use:

defmodule Testmox do

  @calc Application.get_env(:testmox, :calc)

  def hello, do: "Hello #{inspect @calc.add(1, 1)}"

  def bye, do: "Bye #{inspect @calc.mult(2, 2)}"
end

The tests work now, but this is rather contrived at best. What’s the point of having a dumb empty module? If I am stubbing the functions on each expectation, why do I need a module in the first place?

You don’t need to create an empty module. defmock will take care of creating the module, you just need to give it a name for it to use.

2 Likes

But if I don’t create the module, then TestMox.hello and TestMox.bye will fail to compile because they will pickup from Application.get_env a module that doesn’t exist.

I guess this is the part where I use the compile part of Mox:

https://hexdocs.pm/mox/Mox.html#module-compile-time-requirements

Indeed, after reading and applying it, the code compiles and everything works as a charm.

I am still struggling a bit with what Mox is supposed to be, but I guess I get it now. At least I get it better. Thanks!

Going by the documentation it seems to fit the xUnit classification of Configurable Test Double.

2 Likes