Mox v0.3.1 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:

  1. No ad-hoc mocks. You can only create mocks based on behaviours

  2. No dynamic generation of modules during tests. Mocks are preferably defined in your test_helper.exs or in a setup_all block and not per test

  3. Concurrency support. Tests using the same mock can still use async: true

  4. 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:

  1. explicit allowances
  2. 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

Link to this function allow(mock, owner_pid, allowed_pid) View Source

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
Link to this function expect(mock, name, n \\ 1, code) View Source

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.

Link to this function set_mox_from_context(context) View Source

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
Link to this function set_mox_global(context \\ %{}) View Source

Sets the Mox to global mode, where mocks can be consumed by any process.

setup :set_mox_global
Link to this function set_mox_private(context \\ %{}) View Source

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)

stub/3 will overwrite any previous calls to stub/3.

Verifies that all expectations set by the current process have been called.

Verifies that all expectations in mock have been called.

Link to this function verify_on_exit!(context \\ %{}) View Source

Verifies the current process after it exits.