The (not so) Magic Tricks of Testing in Elixir (2/2)

How we use Mox to write better tests in Elixir

In the first part of this post we described various approaches to test Elixir applications, and ended it by pointing out some of the shortcomings of the approaches mentioned, particularly when we need to create mocks. Let’s recap them here:

  • The testing logic is spread out across diferrent files, which makes them hard to reason about;
  • These solutions still allow us to create ad hoc mocks (i.e. mocks that aren’t based on behaviours).

Lately at Onfido we’ve been using the Mox library in some of our Elixir projects, to solve both issues presented above. Mox was created by José Valim, about four months ago. It follows the principles described in his famous blog post Mocks and explicit contracts (by the way, a highly recommended read), and allows the creation of concurrent mocks in Elixir. Its principles are:

  1. No ad-hoc mocks. You can only create mocks based on behaviours
  2. No dynamic generation of modules during tests. Mocks are preferably defined in your test_helper.exs or in a setup_all block and not per test
  3. Concurrency support. Tests using the same mock can still use async: true
  4. Rely on pattern matching and function clauses for asserting on the input instead of complex expectation rules

We will now see how we would test the module described in the first part using the Mox library. First of all, we add it as a dependency:

Then we have to define our mock. This is usually done in the test_helper.exs file, although you can also define them in a setup_all block. In this case we’ll do it in test_helper.exs:

Now we need to change the test itself to start using the library:

First we import Mox so that we can use its functions without using the fully-qualified name. In the test itself, we pipe the mock defined in test_helper.exs to the expect function. This way we’re expecting that emit will be called on our mock module. When this happens, the lambda that is passed as third argument is the function that will be executed. Moreover, we’re using pattern match on the argument of the lambda function, which means that we’re expecting the emit function to be called with "validate_address" . If it doesn’t, the match will fail, along with the test. The body of the lambda function just sends a message to the self()(which returns the PID of the current process), and then we use assert_receive to check that we have that message on the mailbox of the current process.

Lastly, we use setup :verify_on_exit! to ensure that the expectations that we defined on our mocks are met. You can also use verify() or verify(mock_module) if you want to control when the verification is made, and don’t want to do it when the test exists.

We’re essentially following the same strategy as we did in the first part of this post (of sending a message to self()), but now, by using Mox, we have some noteworthy benefits:

  • Our mocks are always defined based on a behaviour. Note that we defined our behaviour for Demo.Events.EventEmitter on the first part of this post. If we hadn’t, Mox will raise an ArgumentError, stating that the module you’re passing to defmock is not a behaviour. This will ensure that the mocks we’re using in our tests are always respecting the contract of the real implementation. As applications grow bigger and bigger, it’s common to see tests where mocks are no longer following the API of the actual implementation. This may be very dangerous, as this leads to running your tests on an unreal environment, which in turn leads to having all of your tests green but things blowing up in your production environment.
  • By using Mox our tests become much more readable, since you can know at a glance what’s happening with that test. In our example, we can quickly know that we’re using a mock, and that when emit is called upon that mock we’re sending a message to the current process. On the example of the first part, the test contained no information about the usage of mocks. You just saw an assert_receive, and to figure out what was going on you had to go to the module under test, see that there was a dependency being injected, go to config/test.exsto know what’s being injected in the test environment, and finally go to that module to see that we’re sending a message to the current process. By having this inlined in our test, we can save all of these jumps and immediately know what’s going on in that test.

What if we’re creating new processes on the module under test?

In the first part there’s a section where we talk about what happens when the module that we’re testing is creating a new process before producing the side-effect we’re looking to confirm. We changed the mock module to a GenServer, so that we can keep track of the list of processes we want to send messages to. Let’s see how we’d achieve the same thing using Mox:

The test itself remains the same, but we had to change two things:

  • Add :set_mox_global to our setup phase. This way any process can consume the mocks defined in our test.
  • Use async: false in our test. Since now our mock may be shared by multiple processes, we have to set this option so that this test doesn’t run concurrently. Note that you can also use :set_mox_from_context in your setup, and Mox will set its own mode to private or global, depending on whether async is set to true or false.

To wrap-up this post, I want to emphasize that, using Mox, you can’t create mocks that aren’t based on behaviours, as its first principle states. This means that you won’t be able to create a mock for a third-party library if it doesn’t define behaviours for its modules. This brings me to this phrase, well-known in TDD communities:

Don’t mock what you don’t own

This phrase is a bit counter intuitive, since most of the mocks you want to create are to control interactions with external parties. But what it actually means is that you shouldn’t just rely on mocks for things that are out of your control.

Let’s explore this further with an example: say that you use the ExTwitter library to access Twitter. In order to follow this principle, you’d create a wrapper around the ExTwitter library, and this wrapper will define the contract between your application and the ExTwitter library. It’s in this wrapper module that you’ll also define the behaviour. Then, the mock that you’ll create is for the wrapper, and not for ExTwitter itself. This wrapper will most of the times be a subset of the ExTwitter library.

This change may cause some friction at first, especially if you’re used to creating ad-hoc mocks, but remember that this will hugely increase the mid- and long-term maintainability of your application. Furthermore, starting to use Mox will surely spark some healthy discussions inside your team about testing in general, and mocks in particular. Take this opportunity to define how and when you’ll use mocks (also taking the Test Pyramid into account), which will ensure that you have a consistent approach to testing throughout your application(s). In turn, this will make your tests easier to follow and also help on the onboarding of new team members.

I hope you’ve enjoyed this post about testing in Elixir. See you next time!

Note: This post is an adaptation of a talk I gave at Lisbon |> Elixir meetup.