Using Mocks Safely in Elixir Tests
or “Stop mocking me! An immutable tale.”
Sooner or later, as your Elixir codebase grows, you’re going to hit a point in your tests where you need to avoid running a particular function. This could be for a couple of reasons:
- True unit tests should only run the module under test. If a function depends on another function in a different module, it makes sense to fake the expected result using a mock.
- Perhaps a function makes an external API call, or has some kind of performance penalty you want to avoid.
For example, let’s take this health-check code.
The code sends a “ping” command to a Redis server via a third-party module, Redix. When our tests run, we don’t want to depend on a Redis server being available — a test-suite should be self-contained.
The first approach I tried was to use a third party testing library called Mock.
First attempt: with_mock
The with_mock function is pretty straightforward. You give it a module name, followed by a keyword list with method names as keys, and dummy functions as values. Then within the do-block, you run your assertions on your code and any references to the mocked module/function are replaced by the dummy.
This is a simple approach, which is easy to reason about. But it comes at a price…
What the…?!
Satisfied with our first attempt, we run the test suite:
$ mix test
………………………………………………………………………………
9 doctests, 81 tests, 0 failuresRandomized with seed 646777
Hoorah! But wait a second, we re-run the tests and…
** (EXIT) no process: the process is not alive or there’s no process currently associated with the given name, possibly because its application isn’t started
9 doctests, 81 tests, 32 failures
One minute the test suite runs fine, and then the next run BAM! it all blows up for no obvious reason. Also, the problem seems pretty serious because processes we expected to be running are apparently not present.
So what on earth is going on here…?
Global Meltdown
Well, one pretty awesome feature of ExUnit is that it can run our tests asynchronously. This means that it’ll use multiple CPU cores and run tests concurrently, reducing the runtime for our suite.
However, since Elixir uses immutable state, it means that any reference to the “Redix” module is going to refer to only that module. Not an instance of it, not a copy, but the actual module itself. That means that, if you replace it with a mock object while another concurrent test is making assertions about behaviour that depends on it… things get ugly.
In this scenario, one of our other tests is doing something that actually expects a full Redix application to be present and responsive, but instead gets back the mock that we defined in our test.
So, quick fix is: set async to be false for this test. This change gets our suite back to green. However, giving up the ability to run tests concurrently is far from ideal. Can we do better?
Second Attempt: Nicer Behaviour
Luckily, there is a way past this and its name is Mox.
Mox is a testing library which takes advantage of a really cool language feature in Elixir: behaviours. But what exactly are behaviours and what are they for?
It’s All Just Polymorphism…
One of the things people often miss in functional languages (especially when they’ve come from an OO background) is the ability to do polymorphism.
Ruby is very dynamic when it comes to the notion of polymorphism in that it really doesn’t care what the class of your object is, so long as it defines the right methods i.e. if it walks like a duck, and quacks like a duck: it is a duck.
Polymorphic behaviour in Elixir can be accomplished by defining a behaviour as a set of method headers using Typespec — you can think of this as similar to an Interface in the Java language.
Both Ruby and Elixir are achieving polymorphic behaviours, but the crucial difference is that Elixir is being explicit about the expected behaviour and formalising a contract that allows us to easily reason about this. If you added the DuckBehaviour to a module and then failed to implement the functions it specified you’d get a compilation warning. In Ruby, you’d get a runtime error.
Mox builds on this concept, as outlined in their documentation:
Using this, we can rewrite our code as follows:
You may notice also we decided to mock the HealthChecks module, rather than the Redix module. This is intentional — it’s a lot easier to mock out modules you own than it is to mock third party modules directly. If in doubt, create a module wrapper to handle the interaction with third party libs. This has the added bonus of not having to refer to Redix from any other part of the codebase!
To Summarise
Using Mox, we can switch the implementation strategy for our behaviours depending on which environment our code is running in. When it runs in the test environment we can load our mock, and we know it conforms to the same set of behaviours as the real module. Due to the way Mox manages processes, it also means we can run our tests asynchronously without worrying about unexpected failures. Hooray! \o/
Further Reading
Like what you see? Join us, we’re hiring. 🚀