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()}
endThen 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
endAdding Mox
Add to mix.exs:
defp deps do
[
{:mox, "~> 1.0", only: :test}
]
endCreating 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.MockPaymentProviderRead 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
endWriting 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
endverify_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
endIf 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)
endGlobal 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
endIn 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.