Functional Mocks with Mox in Elixir

Andrew Hao ·

 

If you’re like me, you came over to Elixir from Ruby and quickly found that certain development assumptions so common to Ruby (and object-oriented programming) require some adjustment in this new functional language.

Recently, I was writing an Elixir-based Alexa skill that needed to reach out to an external service, the Alexa Device Address API. I needed to find the right way to abstract away this concern in a testable manner:


module MyApp.Alexa do
def call do
# …
get_country_and_zip_code(device_id, consent_token)
|> do_some_other_transformation
# …
end
defp get_country_and_zip_code(device_id, consent_token) do
url =
"https://api.amazonalexa.com/v1/devices/#{device_id}/settings/address/countryAndPostalCode"
headers = [{"Authorization", "Bearer #{consent_token}"}, {"Accept", "application/json"}]
HTTPoison.get!(url, headers)
|> Map.get(:body)
|> Poison.decode!()
end
end

view raw

1.ex

hosted with ❤ by GitHub

However, writing tests for this function is very difficult. It calls a live API, which renders these tests difficult to set up, slow, and potentially brittle. It mixes both domain-specific data transformation functions with calls to an external API system we do not control. How can we manage this complexity?

Push I/O to the edges with wrapped modules

When designing applications with a layered approach, we try to protect our core domain logic from needing to know about the outside world. These actions, like accessing a database, fetching from a remote API, or receiving inputs from a web service request, are only done at the “edges” of the transaction and wrapped in modules that we own. This is often referred to as a “hexagonal” or a “functional” architecture. Doing so allows our domain logic to remain isolated from impure concerns, and our tests to be cleanly written with deterministic expectations.

So let’s push the HTTP concerns to the outer edges of our design and wrap our HTTP service in its own module, allowing the rest of our app code to collaborate with a module that we own instead of needing to access the messy, unpredictable outside world:


module MyApp.Alexa do
def call do
# …
MyApp.AlexaDeviceApi.get_country_and_zip_code(device_id, consent_token)
|> do_some_other_transformation
# …
end
end
module MyApp.AlexaDeviceApi do
def get_country_and_zip_code(device_id, consent_token) do
url =
"https://api.amazonalexa.com/v1/devices/#{device_id}/settings/address/countryAndPostalCode"
headers = [{"Authorization", "Bearer #{consent_token}"}, {"Accept", "application/json"}]
HTTPoison.get!(url, headers)
|> Map.get(:body)
|> Poison.decode!()
end
end

view raw

2.ex

hosted with ❤ by GitHub

That feels better! The AmazonDeviceApi module now has the exclusive responsibility of wrapping communication with an external API, and now all collaborating modules merely have to deal with the wrapper module, which is under our control.

Now, how are we now to write tests for the original Alexa module? The Alexa module becomes a collaborator with the AmazonDeviceApi module, but we have no way to get to it in our tests:


# test/alexa_test.ex
module MyApp.AlexaTest do
test "call/0 calling the amazon device service" do
# Uh oh, how do I intercept the call to AlexaDeviceApi.get_country_and_zip_code/2?
end
end

view raw

2_tests.ex

hosted with ❤ by GitHub

Ruby mocking versus Elixir mocks

If you were like me and came from Ruby, its dynamic nature allowed us to modify classes and objects at runtime and install mocks from within our tests. RSpec allowed (and encouraged) us to gloriously assert method invocations and stub out methods at test runtime.

When many of us in the Ruby ecosystem came over to Elixir, we felt the tug to bring those same habits to our Elixir code. There are many mocking frameworks that help us try to recreate that experience in Elixir, but wait! Not so fast.

Early last year, José Valim wrote a blog post called “Mocks and Explicit Contracts” where he came out strongly against implementing mocks in a manner that dynamically modified global modules. He comes out against mocking globals, because “…when you use mock as a verb, you are changing something that already exists, and often those changes are global.” The act of rewiring your program with metaprogramming magic for the sake of a test is, to José, considered harmful. Additionally, it prevents tests from running asynchronously and in parallel.

Instead, he advocates creating Elixir Behaviours for your module interface, then creating test mocks that conform to the interfaces that are swapped in during tests. The code is more readable, easier to reason about, and easier to test.

Let’s try that out by first creating a behavior for our HTTP client, then implementing it in our module:


module MyApp.Alexa do
# Note how this becomes a module method, and the configuration is pulled from the app config
@alexa_device_api Application.get_env(:my_app, :alexa_device_api)
def call do
@alexa_device_api.get_country_and_zip_code(device_id, consent_token)
|> do_some_other_transformation
# …
end
end
# AlexaDeviceApi becomes a behaviour that describes an interface to conform to
module MyApp.AlexaDeviceApi do
@callback get_country_and_zip_code(device_id :: String.t(), consent_token :: String.t()) ::
{:ok, map()}
end
# The implementation moves to AlexaDeviceApi.Http, the live implementation
# of the API wrapper.
module MyApp.AlexaDeviceApi.Http do
@behaviour MyApp.AlexaDeviceApi
def get_country_and_zip_code(device_id, consent_token) do
url =
"https://api.amazonalexa.com/v1/devices/#{device_id}/settings/address/countryAndPostalCode"
headers = [{"Authorization", "Bearer #{consent_token}"}, {"Accept", "application/json"}]
HTTPoison.get!(url, headers)
|> Map.get(:body)
|> Poison.decode!()
end
end
# We configure the application to deliver the live, HTTP-based implementation by default
# config/config.exs
config :myapp, :alexa_device_api, MyApp.AlexaDeviceApi.HttpClient

view raw

3.ex

hosted with ❤ by GitHub

Note how we fetch the correct implementation of the AlexaDeviceApi via a runtime config variable lookup. This will become important as we turn to our tests, where we will supply a mock to the caller context.

Enter mox. Mox is a library that allows you to define test double behavior at test definition time. Let’s see how that works:


# We define a MockClient in our test_helper…
# test/test_helper.ex
Mox.defmock(MyApp.AlexaDeviceApi.MockClient, for: MyApp.AlexaDeviceApi)
# …configure it to be picked up by the app when it runs in test mode
# config/test.exs
config :myapp, :alexa_device_api, MyApp.AlexaDeviceApi.MockClient
# …and define behaviors for it per test
# test/alexa_test.ex
module MyApp.AlexaTest do
# …
import Mox
# This makes us check whether our mocks have been properly called at the end
# of each test.
setup :verify_on_exit!
test "call/0 calls the amazon device service, then transforms the values" do
# Here, we configure this MockClient to always return a hardcoded value
MyApp.AlexaDeviceApi.MockClient
|> expect(:get_country_and_zip_code, fn _, _ ->
{:ok, %{country: "USA", postal_code: "94105"}}
end)
assert MyApp.AlexaTest.call() == "some expected value"
end
end

view raw

3_tests.ex

hosted with ❤ by GitHub

Tada! Now our tests inside the domain boundaries run fast because they run up against in-memory mocks. And we can write a simple integration test that verifies that the actual calls to the live API are working.

Additionally, the use of a behaviour enforces that our test and our live implementations never fall out of sync – they are required to conform to the same interface.

Some finer points of mocks and stubs

I always like to refer back to Martin Fowler’s Mocks Aren’t Stubs article for clearer definition on our test components.

Jose’s article refers to the use of a “mock as a noun” – which I would clarify to be a test fake. He advocates creating static, preprogrammed mock modules with canned responses.

The Mox library allows us to write our test objects both as fakes – modules with canned, preprogrammed responses, but also as mocks – modules that verify calls on functions with specific values.

Here we define a mock verification routine in our test, using the Mox mock:


# This is a mock, because it verifies the function call specifically
# with the values of the arguments passed to it.
MyApp.AlexaDeviceApi.MockClient
|> expect(:get_country_and_zip_code, fn "abcdef", "12345" ->
{:ok, %{country: "USA", postal_code: "94105"}}
end)
# …
verify!(MyApp.AlexaDeviceApi.MockClient)
# or: verify_on_exit!
# This, too, is a mock, because it verifies the function call, ignoring the args passed to it
MyApp.AlexaDeviceApi.MockClient
|> expect(:get_country_and_zip_code, fn _, _ ->
{:ok, %{country: "USA", postal_code: "94105"}}
end)
# …
verify!(MyApp.AlexaDeviceApi.MockClient)
# or: verify_on_exit!

view raw

4_mocks.ex

hosted with ❤ by GitHub

The notable thing here is that the verification of the function call is written into our test.

And here we use a Mox stub, (what Fowler calls a fake):


# This is a fake, or a stub. It discards all inputs and returns
# pre-canned output. It does not care whether the function was actually
# called, just that if it ever was, it should return.
MyApp.AlexaDeviceApi.MockClient
|> stub(:get_country_and_zip_code, fn _, _ ->
{:ok, %{country: "USA", postal_code: "94105"}}
end)

view raw

4_stubs.ex

hosted with ❤ by GitHub

The notable thing here is that we do not care whether the test function is invoked or not – merely that if it were ever to be called, it should always return a canned value.

Should you use a stub or a mock? It depends on your requirements in your test. Is the called function (collaborating function) a critical part of your module’s behavior, and is the data passed to it important to verify? You should use a mock function. Is calling the collaborating function merely a side effect of calling your subject and not the main verification purpose of your test? You may want to use a stub.

When should I use Mox?

This process is notably nicer than our original approach, but it still requires a lot of ceremony to wire up and get running in your application. There’s a lot of configuration and boilerplate involved. Clearly, this will become a pain if every module is required to define its own mock. So when should you use Mox mocks?

I suggest using mocks at context (system) boundaries – for example, the top-level module of each application context if you’re using a Phoenix context. A mock can hide away complicated implementation details of the entire system behind it, and the usage of a mock can also drive your design to unify behind a coherent, single interface. Collaborating callers can then use it to implement their own collaboration behaviors with this system.

If you are the downstream consumer of a software system that you may not control, you should be implementing a mock at the outermost entry point to that system.

When should I not use Mox?

Mocks are clearly not appropriate for every module you ever write! How, then should we be writing tests for functions that call across module boundaries, but the two modules are closely-related within the same system?

Tune in to our next blog post to discover some other ways to decouple your modules in a lightweight, test-driven fashion. Got comments? Let us know on Twitter!

Andrew Hao
Andrew Hao

Andrew is a design-minded developer who loves making applications that matter.