This post is clearing up confusions around mocking in Elixir. If you were ever confused about mocks and stubs in Elixir, I made it 100% clear for you.
Mocking is the testing technique to replace underlying code behaviour with the response we want. Typically we use it to mock modules that depend on 3rd-party services, APIs, internet connection, or system dependencies. Mox is my go-to library for mocking in Elixir.
Note that the distinction between mocks and stubs is highly inconsistent across the literature. I am using my own understanding of the terms that is aligned with the Mox documentation.
Stubs
Before we dive into mocking, let’s define stubs and how we can use them without any mocking at all.
According to Wikipedia a stub is:
A method stub or simply stub in software development is a piece of code used to stand in for some other programming functionality.
I like to remember it as a counterfoil of a document in the physical world.
Replacing the real modules with stubs is as easy as using dependency injection or simply taking advantage of the environment configuration:
# config/config.exs
config :my_app, Subscriptions, stripe_service: MyApp.StripeService
# config/test.exs
config :my_app, Subscriptions, stripe_service: MyApp.StripeServiceStub
# lib/my_app/subscriptions.ex
defmodule MyApp.Subscriptions do
@stripe_service Application.get_env(:my_app, __MODULE__)[:stripe_service]
def charge_customer(customer_id) do
@stripe_service.charge_customer(customer_id)
...
We can also use a test_helper.exs
file:
Application.put_env(:my_app, :stripe_service, MyApp.StripeServiceStub)
MyApp.StripeService
could be our wrapper around Stripity Stripe library accessing stripe.com. StripeServiceStub
would be a module not too different from MyApp.StripeService
returning predefined responses.
The idea is that all the code that needs Stripe working will be satisfied with a generic response. It’s straightforward.
I like to put such modules in test/support/stubs/
directory with the full path and a name with appended Stub
. This is just my convention.
If we want to stay organized, we should also define common behaviour:
# lib/my_app/subscriptions/stripe_service.ex
defmodule MyApp.Subscriptions.StripeService do
@behaviour MyApp.Subscriptions.StripeServiceBehaviour
# test/support/stubs/stripe_service_stub.ex
defmodule MyApp.Subscriptions.StripeServiceStub do
@behaviour MyApp.Subscriptions.StripeServiceBehaviour
# lib/my_app/subscriptions/stripe_service_behaviour.ex
defmodule MyApp.Subscriptions.StripeServiceBehaviour do
@callback charge_customer(integer()) :: tuple()
This behaviour will also become crucial when using Mox.
Mocks
Most of the time, stubs won’t be enough because we want some flexibility. If we call MyApp.Subscriptions.charge_customer
, we might want to adjust the response based on the scenario we are working with. More likely than not, we want to test all possible error responses.
Mock objects in object-oriented programming are mimicking the behaviour of the actual objects. Similarly, mocks in Mox mimick the behaviour of modules and their functions.
I like to remember mocking as mimicking someone in the physical world.
When introducing Mox, we should think about stubs as our defaults. I like to put a typical success path into the stub. Stubs are great for this because I can use 100 stubs across the whole system and still test a module at hand at any level of abstractions.
So, how to transform from plain stubs to stubs with Mox? First, we should write our behaviour as above. Then we can to define a mock module for our behaviour in test/stubs/mox.ex
as follows:
# test/stubs/mox.ex
Mox.defmock(MyApp.Subscriptions.StripeServiceMock,
for: MyApp.Subscriptions.StripeServiceBehaviour
)
The mock module can be named as you wish, but I like to keep things organized by appending Mock
.
This mock module is a module Mox can now use to mimick the behaviour we predefined. With this change, we could import Mox in a test file and start mocking.
To make a fallback for our mock, we have to tell Mox to default to the stub module:
# test/support/stripe_service_mock.ex
defmodule StripeServiceMock do
use ExUnit.CaseTemplate
setup do
Mox.stub_with(
MyApp.Subscriptions.StripeServiceMock,
MyApp.Subscriptions.StripeServiceStub
)
# more stub_with/2 calls possible
:ok
end
end
Awesome, now we are ready to change our test config to the mock module:
# config/test.exs
config :my_app, Subscriptions, stripe_service: MyApp.StripeServiceMock
Mocking
We configured a mock module in tests for our MyApp.Subscriptions
module. We also told Mox to default to the stub module. We are ready to write some tests, aren’t we?
defmodule MyApp.SubscriptionsTest do
...
alias MyApp.Subscriptions
use StripeServiceMock, async: true
describe "charge_customer/1" do
setup :create_customer
test "charges the right amount", %{customer: customer} do
{:ok, charge} = Subscriptions.charge_customer(customer.id)
# asserts
end
end
By using the StripeServiceMock
module, we are automatically defaulting to the stub modules as if we would specify it in the config directly. This is especially useful for higher level code that is not even concerned with Stripe at all.
What about the errors?
If we import Mox, we can mock any specific function.
defmodule MyApp.SubscriptionsTest do
...
alias MyApp.Subscriptions
import Mox
use StripeServiceMock, async: true
describe "charge_customer/1" do
setup :create_customer
test "charges the right amount", %{customer: customer} do
...
end
test "returns an error", %{customer: customer} do
MyApp.Subscriptions.StripeServiceMock
|> stub(:charge_customer, fn _customer_id ->
error = %{...}
{:error, error}
end)
{:error, error} = Subscriptions.charge_customer(customer.id)
# asserts
end
end
By piping the mock module to Mox.stub/2
we are able to mimick the behaviour of charge_customer/1
with another stub.
Expectations
Sometimes we want to assert that a particular service function is called. We can achieve this by replacing the stub
call with an expect
call which takes an extra argument to determine how many times the particular function should be called (1
in the example below):
defmodule MyApp.SubscriptionsTest do
...
alias MyApp.Subscriptions
import Mox
use StripeServiceMock, async: true
setup :verify_on_exit!
describe "charge_customer/1" do
setup :create_customer
test "charges the right amount", %{customer: customer} do
...
end
test "returns an error", %{customer: customer} do
MyApp.Subscriptions.StripeServiceMock
|> expect(:charge_customer, 1, fn _customer_id ->
error = %{...}
{:error, error}
end)
{:error, error} = Subscriptions.charge_customer(customer.id)
# asserts
end
end
Together with the :verify_on_exit!
callback the calls will be verified.
All expectations live within the current process, so you can use them in parallel tests without issues. Also don’t forget you can make better stubs and expectations by pattern-matching the respective function arguments. For example, the _customer_id
could be an actual ID of the created customer. If another customer is passed, the expectation fails.
Get Test Driving Rails while it's in prerelease.