Mox v0.3.0 Mox View Source
Mox is a library for defining concurrent mocks in Elixir.
The library follows the principles outlined in “Mocks and explicit contracts”, summarized below:
No ad-hoc mocks. You can only create mocks based on behaviours
No dynamic generation of modules during tests. Mocks are preferably defined in your
test_helper.exs
or in asetup_all
block and not per testConcurrency support. Tests using the same mock can still use
async: true
Rely on pattern matching and function clauses for asserting on the input instead of complex expectation rules
Example
As an example, imagine that your library defines a calculator behaviour:
defmodule MyApp.Calculator do
@callback add(integer(), integer()) :: integer()
@callback mult(integer(), integer()) :: integer()
end
If you want to mock the calculator behaviour during tests, the first step
is to define the mock, usually in your test_helper.exs
:
Mox.defmock(MyApp.CalcMock, for: MyApp.Calculator)
Once the mock is defined, you can pass it to the system under the test. If the system under test relies on application configuration, you should also set it before the tests starts to keep the async property. Usually in your config files:
config :my_app, :calculator, MyApp.CalcMock
Or in your test_helper.exs
:
Application.put_env(:my_app, :calculator, MyApp.CalcMock)
Now in your tests, you can define expectations and verify them:
use ExUnit.Case, async: true
import Mox
# Make sure mocks are verified when the test exits
setup :verify_on_exit!
test "invokes add and mult" do
MyApp.CalcMock
|> expect(:add, fn x, y -> x + y end)
|> expect(:mult, fn x, y -> x * y end)
assert MyApp.CalcMock.add(2, 3) == 5
assert MyApp.CalcMock.mult(2, 3) == 6
end
All expectations are defined based on the current process. This means multiple tests using the same mock can still run concurrently.
Compile-time requirements
If the mock needs to be available during the project compilation, for
instance because you get undefined function warnings, then instead of
defining the mock in your test_helper.exs
, you should instead define
it under test/support/mocks.ex
:
Mox.defmock(MyApp.CalcMock, for: MyApp.Calculator)
Then you need to make sure that files in test/support
get compiled
with the rest of the project. Edit your mix.exs
file to add test/support
directory to compilation paths:
def project do
[
...
elixirc_paths: elixirc_paths(Mix.env),
...
]
end
defp elixirc_paths(:test), do: ["test/support", "lib"]
defp elixirc_paths(_), do: ["lib"]
Multi-process collaboration
Mox supports multi-process collaboration via two mechanisms:
- explicit allowances
- global mode
The allowance mechanism can still run tests concurrently while the global ones doesn’t. We explore both next.
Explicit allowances
An allowance permits a child process to use the expectations and stubs defined in the parent process while still being safe for async tests.
test "invokes add and mult from a task" do
MyApp.CalcMock
|> expect(:add, fn x, y -> x + y end)
|> expect(:mult, fn x, y -> x * y end)
parent_pid = self()
Task.async(fn ->
MyApp.CalcMock |> allow(parent_pid, self())
assert MyApp.CalcMock.add(2, 3) == 5
assert MyApp.CalcMock.mult(2, 3) == 6
end)
|> Task.await
end
Global mode
Mox supports global mode, where any process can consume mocks and stubs defined in your tests. To manually switch to global mode use:
set_mox_global()
which can be done as a setup callback:
setup :set_mox_global
test "invokes add and mult from a task" do
MyApp.CalcMock
|> expect(:add, fn x, y -> x + y end)
|> expect(:mult, fn x, y -> x * y end)
Task.async(fn ->
assert MyApp.CalcMock.add(2, 3) == 5
assert MyApp.CalcMock.mult(2, 3) == 6
end)
|> Task.await
end
The default mode is private
and the global mode must always be explicit
set per test.
You can also automatically choose global or private mode depending on
if your tests run in async mode or not with. In such case Mox will use
private mode when async: true
, global mode otherwise:
setup :set_mox_from_context
Link to this section Summary
Functions
Allows other processes to share expectations and stubs defined by owner process
Defines a mock with the given name :for
the given behaviour
Defines that the name
in mock
with arity given by
code
will be invoked n
times
Chooses the Mox mode based on context. When async: true
is used
the mode is :private
, otherwise :global
is chosen
Sets the Mox to global mode, where mocks can be consumed by any process
Sets the Mox to private mode, where mocks can be set and consumed by the same process unless other processes are explicitly allowed
Defines that the name
in mock
with arity given by
code
can be invoked zero or many times
Verifies that all expectations set by the current process have been called
Verifies that all expectations in mock
have been called
Verifies the current process after it exits
Link to this section Functions
Allows other processes to share expectations and stubs defined by owner process.
Examples
To allow child_pid
to call any stubs or expectations defined for MyMock
:
allow(MyMock, self(), child_pid)
Defines a mock with the given name :for
the given behaviour.
Mox.defmock MyMock, for: MyBehaviour
Defines that the name
in mock
with arity given by
code
will be invoked n
times.
Examples
To allow MyMock.add/2
to be called once:
expect(MyMock, :add, fn x, y -> x + y end)
To allow MyMock.add/2
to be called five times:
expect(MyMock, :add, 5, fn x, y -> x + y end)
expect/4
can also be invoked multiple times for the same
name/arity, allowing you to give different behaviours on each
invocation.
Chooses the Mox mode based on context. When async: true
is used
the mode is :private
, otherwise :global
is chosen.
setup :set_mox_from_context
Sets the Mox to global mode, where mocks can be consumed by any process.
setup :set_mox_global
Sets the Mox to private mode, where mocks can be set and consumed by the same process unless other processes are explicitly allowed.
setup :set_mox_private
Defines that the name
in mock
with arity given by
code
can be invoked zero or many times.
Opposite to expectations, stubs are never verified.
If expectations and stubs are defined for the same function and arity, the stub is invoked only after all expectations are fulfilled.
Examples
To allow MyMock.add/2
to be called any number of times:
stub(MyMock, :add, fn x, y -> x + y end)
Verifies that all expectations set by the current process have been called.
Verifies that all expectations in mock
have been called.
Verifies the current process after it exits.