RSS

Understanding Elixir mocking with Mox

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.

← THIS CHRISTMAS

I am writing an introductory book on web application deployment. Networking, processes, systemd, backups, and all your usual suspects.

Open →