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:
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:
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:
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:
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:
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:
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):
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!