Mox for Elixir: Mocking and Dependency Injection in Tests

Mox for Elixir: Mocking and Dependency Injection in Tests

Testing code that calls external services — payment processors, email providers, third-party APIs — presents a challenge. You don't want real network calls in your test suite, but you also don't want to lose confidence in how your code handles those interactions. Mox, Elixir's standard mocking library, solves this cleanly.

Why Mox, Not Just Test Stubs?

You could write simple test stubs as separate modules. The problem is stubs lie — they let you call any function, with any arguments, and return whatever you configure. You can have a stub that diverges from the real implementation without knowing it.

Mox takes a different approach: mocks must implement a behaviour. Elixir behaviours are explicit contracts (like interfaces in other languages). Mox verifies that your mock implements the same contract as the real module, and that your code under test only calls functions that actually exist.

Defining a Behaviour

Before creating a mock, define the behaviour your real module implements:

defmodule MyApp.PaymentProvider do
  @callback charge(amount :: integer(), card_token :: String.t()) ::
    {:ok, String.t()} | {:error, String.t()}

  @callback refund(charge_id :: String.t()) ::
    {:ok, String.t()} | {:error, String.t()}
end

Then make your real implementation declare it:

defmodule MyApp.StripeProvider do
  @behaviour MyApp.PaymentProvider

  def charge(amount, card_token) do
    # Real Stripe API call
  end

  def refund(charge_id) do
    # Real Stripe API call
  end
end

Adding Mox

Add to mix.exs:

defp deps do
  [
    {:mox, "~> 1.0", only: :test}
  ]
end

Creating Mocks

Define mocks in test/test_helper.exs:

ExUnit.start()

Mox.defmock(MyApp.MockPaymentProvider, for: MyApp.PaymentProvider)

Mox.defmock/2 creates a module MyApp.MockPaymentProvider that implements MyApp.PaymentProvider. If the real behaviour changes, Mox will fail to create the mock until you update it too.

Dependency Injection

For mocks to work, your production code needs to be written to accept the implementation as a dependency. The cleanest way in Elixir is via application config:

# config/config.exs
config :my_app, :payment_provider, MyApp.StripeProvider

# config/test.exs
config :my_app, :payment_provider, MyApp.MockPaymentProvider

Read the configured module in your code:

defmodule MyApp.Orders do
  defp payment_provider do
    Application.get_env(:my_app, :payment_provider)
  end

  def charge_customer(order) do
    payment_provider().charge(order.total, order.card_token)
  end
end

Writing Tests with Mox

Now in your tests, set expectations before calling the code under test:

defmodule MyApp.OrdersTest do
  use ExUnit.Case, async: true
  import Mox

  # Ensures mocks are verified when test exits
  setup :verify_on_exit!

  test "charges customer and creates order" do
    expect(MyApp.MockPaymentProvider, :charge, fn amount, token ->
      assert amount == 999
      assert token == "tok_test_123"
      {:ok, "ch_stripe_abc"}
    end)

    order = %{total: 999, card_token: "tok_test_123"}
    assert {:ok, completed_order} = MyApp.Orders.charge_customer(order)
    assert completed_order.charge_id == "ch_stripe_abc"
  end

  test "handles payment failure" do
    expect(MyApp.MockPaymentProvider, :charge, fn _amount, _token ->
      {:error, "Your card was declined"}
    end)

    order = %{total: 999, card_token: "tok_bad"}
    assert {:error, "Payment failed: Your card was declined"} =
      MyApp.Orders.charge_customer(order)
  end
end

verify_on_exit! is critical — it ensures every expect was actually called. If your code never calls the mocked function, the test fails. This catches bugs where code paths are silently skipped.

Allowing Multiple Calls

Use stub/3 when you want to allow calls without requiring them:

stub(MyApp.MockPaymentProvider, :charge, fn _amount, _token ->
  {:ok, "ch_stub"}
end)

Use expect/4 with a count for functions called multiple times:

# Expect exactly 3 calls
expect(MyApp.MockPaymentProvider, :charge, 3, fn amount, _token ->
  {:ok, "ch_#{amount}"}
end)

Async Tests and Mox

Mox is designed for async tests. Expectations are per-process, so tests running in parallel don't interfere with each other:

defmodule MyApp.OrdersTest do
  use ExUnit.Case, async: true
  import Mox

  setup :verify_on_exit!

  # Each test's expectations are isolated to that test process
  test "concurrent test 1" do
    expect(MyApp.MockPaymentProvider, :charge, fn _, _ -> {:ok, "ch_1"} end)
    # ...
  end

  test "concurrent test 2" do
    expect(MyApp.MockPaymentProvider, :charge, fn _, _ -> {:ok, "ch_2"} end)
    # ...
  end
end

If code in a different process (like a spawned Task or GenServer) needs to use the mock, allow it explicitly:

test "async process uses mock" do
  parent = self()

  stub(MyApp.MockPaymentProvider, :charge, fn _, _ -> {:ok, "ch_ok"} end)

  task = Task.async(fn ->
    Mox.allow(MyApp.MockPaymentProvider, parent, self())
    MyApp.Orders.charge_in_background(%{total: 100, card_token: "tok"})
  end)

  Task.await(task)
end

Global Mode for Integration Tests

For tests that span multiple processes and where per-process setup is impractical, use global mode:

setup do
  Mox.set_mox_global()
  :ok
end

In global mode, all processes see the same stubs. Use this sparingly and only in non-async tests.

Common Patterns

Default stubs for happy paths: If most tests share a common happy-path response, set a default stub in setup and override with expect in tests that need different behavior.

Verify call arguments: Put assertions inside the mock function to verify your code passes the right arguments:

expect(MyApp.MockProvider, :notify, fn user_id, message ->
  assert user_id == 42
  assert message =~ "Payment confirmed"
  :ok
end)

Combine with real implementations: Use passthrough from :meck or mix real and mock modules for partial mocking — though this is often a sign to reconsider your design.

Testing End-to-End Flows

Mox handles unit isolation well. For verifying that your entire Phoenix application works end-to-end — with real browser interactions, correct error pages, and payment confirmation flows — HelpMeTest complements this approach. You can write high-level user flow tests while letting Mox handle the external service isolation at the unit level.

The two approaches stack well: Mox tests confirm each component handles responses correctly, and end-to-end tests confirm the entire user journey works as expected.

Summary

Mox enforces a discipline that makes Elixir code more testable by design:

  • Behaviours define the contract
  • Dependency injection makes swapping implementations possible
  • Expectations verify your code actually calls what it should
  • verify_on_exit! catches silently skipped code paths

The result is a test suite that gives you genuine confidence in how your code interacts with external services, without making a single real API call.

Read more