Mox v0.5.2 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 with defmock/2, usually in your test_helper.exs:

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

Now in your tests, you can define expectations with expect/4 and verify them via verify_on_exit!/1:

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

In practice, you will have to pass the mock 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)

All expectations are defined based on the current process. This means multiple tests using the same mock can still run concurrently unless the Mox is set to global mode. See the "Multi-process collaboration" section.

Especially if you put the mock into the config/application environment you might want the implementation to fall back to the original implementation when no expectations are defined. stub_with/2 is just what you need! Given that MyApp.TestCalculator is the implementation you are mocking you can do the following in your test (for instance inside setup):

Mox.stub_with(MyApp.CalcMock, MyApp.TestCalculator)

Now, if no expectations are defined it will call the implementation in MyApp.TestCalculator.

Multiple behaviours

Mox supports defining mocks for multiple behaviours.

Suppose your library also defines a scientific calculator behaviour:

defmodule MyApp.ScientificCalculator do
  @callback exponent(integer(), integer()) :: integer()
end

You can mock both the calculator and scientific calculator behaviour:

Mox.defmock(MyApp.SciCalcMock, for: [MyApp.Calculator, MyApp.ScientificCalculator])

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 the 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 one 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

Note: if you're running on Elixir 1.8.0 or greater and your concurrency comes from a Task then you don't need to add explicit allowances. Instead $callers is used to determine the process that actually defined the expectations.

Global mode

Mox supports global mode, where any process can consume mocks and stubs defined in your tests. This is enabled automatically based if the test is async or not:

setup :set_mox_from_context
setup :verify_on_exit!

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

Blocking on expectations

If your mock is called in a different process than the test process, in some cases there is a chance that the test will finish executing before it has a chance to call the mock and meet the expectations. Imagine this:

test "calling a mock from a different process" do
  expect(MyApp.CalcMock, :add, fn x, y -> x + y end)

  spawn(fn -> MyApp.CalcMock.add(4, 2) end)

  verify!()
end

The test above has a race condition because there is a chance that the verify!/0 call will happen before the spawned process calls the mock. In most cases, you don't control the spawning of the process so you can't simply monitor the process to know when it dies in order to avoid this race condition. In those cases, the way to go is to "sync up" with the process that calls the mock by sending a message to the test process from the expectation and using that to know when the expectation has been called.

test "calling a mock from a different process" do
  parent = self()
  ref = make_ref()

  expect(MyApp.CalcMock, :add, fn x, y ->
    send(parent, {ref, :add})
    x + y
  end)

  spawn(fn -> MyApp.CalcMock.add(4, 2) end)

  assert_receive {^ref, :add}

  verify!()
end

This way, we'll wait until the expectation is called before calling verify!/0.

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(s).

Expects the name in mock with arity given by code to be invoked n times.

Chooses the Mox mode based on context.

Sets the Mox to global mode.

Sets the Mox to private mode.

Allows the name in mock with arity given by code to be invoked zero or many times.

Stubs all functions described by the shared behaviours in the mock and module.

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_via)

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)

allow/3 also accepts named process or via references:

allow(MyMock, self(), SomeChildProcess)

Defines a mock with the given name :for the given behaviour(s).

Mox.defmock(MyMock, for: MyBehaviour)

With multiple behaviours:

Mox.defmock(MyMock, for: [MyBehaviour, MyOtherBehaviour])

Skipping optional callbacks

By default, functions are created for all callbacks, including all optional callbacks. But if for some reason you want to skip optional callbacks, you can provide the list of callback names to skip (along with their arities) as :skip_optional_callbacks:

Mox.defmock(MyMock, for: MyBehaviour, skip_optional_callbacks: [on_success: 2])

This will define a new mock (MyMock) that has a defined function for each callback on MyBehaviour except for on_success/2. Note: you can only skip optional callbacks, not required callbacks.

You can also pass true to skip all optional callbacks, or false to keep the default of generating functions for all optional callbacks.

Passing @moduledoc

You can provide value for @moduledoc with :moduledoc option.

Mox.defmock(MyMock, for: MyBehaviour, moduledoc: false)
Mox.defmock(MyMock, for: MyBehaviour, moduledoc: "My mock module.")
Link to this function

expect(mock, name, n \\ 1, code)

View Source

Expects the name in mock with arity given by code to be invoked n times.

If you're calling your mock from an asynchronous process and want to wait for the mock to be called, see the "Blocking on expectations" section in the module documentation.

When expect/4 is invoked, any previously declared stub for the same name and arity will be removed. This ensures that expect will fail if the function is called more than n times. If a stub/3 is invoked afterexpect/4 for the same name and arity, the stub will be used after all expectations are fulfilled.

Examples

To expect MyMock.add/2 to be called once:

expect(MyMock, :add, fn x, y -> x + y end)

To expect MyMock.add/2 to be called five times:

expect(MyMock, :add, 5, fn x, y -> x + y end)

To expect MyMock.add/2 to not be called:

expect(MyMock, :add, 0, fn x, y -> :ok end)

expect/4 can also be invoked multiple times for the same name/arity, allowing you to give different behaviours on each invocation:

MyMock
|> expect(:add, fn x, y -> x + y end)
|> expect(:add, fn x, y -> x * y end)
Link to this function

set_mox_from_context(context)

View Source

Chooses the Mox mode based on context.

When async: true is used, set_mox_private/1 is called, otherwise set_mox_global/1 is used.

Examples

setup :set_mox_from_context
Link to this function

set_mox_global(context \\ %{})

View Source

Sets the Mox to global mode.

In global mode, mocks can be consumed by any process.

An ExUnit case where tests use Mox in global mode cannot be async: true.

Examples

setup :set_mox_global
Link to this function

set_mox_private(context \\ %{})

View Source

Sets the Mox to private mode.

In private mode, mocks can be set and consumed by the same process unless other processes are explicitly allowed.

Examples

setup :set_mox_private

Allows the name in mock with arity given by code to be invoked zero or many times.

Unlike 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.

Stubs all functions described by the shared behaviours in the mock and module.

Examples

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

defmodule TestCalculator do
  @behaviour Calculator
  def add(a, b), do: a + b
  def mult(a, b), do: a * b
end

defmock(CalcMock, for: Calculator)
stub_with(CalcMock, TestCalculator)

This is the same as calling stub/3 for each behaviour in CalcMock:

stub(CalcMock, :add, &TestCalculator.add/2)
stub(CalcMock, :mult, &TestCalculator.mult/2)

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.

If you want to verify expectations for all tests, you can use verify_on_exit!/1 as a setup callback:

setup :verify_on_exit!