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 |
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?
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 |
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 |
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 |
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 |
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.
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! |
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) |
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.
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.
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 is a design-minded developer who loves making applications that matter.