Elixir Test Mocking with Mox

Building an api client mock and learning to love mocks-as-nouns

Sophie DeBenedetto
Mar 20, 2019 · 7 min read

Why We Need Mox

This consideration often leads us to reach for a test mock. However, we want to be careful of how we think about “mocking”. When we mock certain interactions by creating unique function stubs in a given test example, we establish a dangerous pattern. We couple the run of our tests to the behavior of a particular dependency, like an API client. We avoid defining shared behavior among our stubbed functions. We make it harder to iterate on our tests.

Instead, Jose Valim advocates that we change the way we think about test mocks; that we think about a mock as a noun, instead of a verb:

Mocks are simulated entities that mimic the behavior of real entities in controlled ways…I always consider “mock” to be a noun, never a verb.

This reconceptualization forces us to create mock entities that our app is configured to use in the test environment, rather than mocking function calls with canned responses in our test examples.

In our first attempt at applying this advice to tests for a GitHub API client app that we built in Elixir, we hand-rolled our own test server. Our test server acted as a stand-in for the GitHub API in our test environment and it implemented a controller that responded with the expected payloads under the given conditions. Our test server became our mock. As a result, all of the test examples that made calls to this mock API implicitly mocked (there’s that verb again) those interactions.

This obfuscated mocking had some serious drawbacks. In order to write new tests that relied on GitHub API interactions, a developer would have to be aware of the test controller and comb through the test controller code to find the payload that their code is expected to operate on in a given test example.

The existence of a GitHub API mock server also allowed us to avoid defining an explicit contract for the api client that we were testing. Since we never had to mock the api client itself, it wasn’t strictly necessary to define an explicit interface for the behavior of that client.

By leveraging the Mox library to define mocks for our api client, we were able to address both of these drawbacks. This resulted in easy-to-read and easy-to-iterate on tests that allow each developer to define their own expectations against the mock client. It also forced us to define an explicit set of behaviors for our api client, in order for Mox to mock it.

Keep reading to see how we did it.

Getting Started

{:mox, "~> 0.5.0", only: :test}

Mocking our Github Api Client

Image for post
Image for post

Defining a Behaviour

The module responsible for talking to the GitHub API is GithubClient.ApiClient. We want to write tests for areas of our code that use GithubClient.ApiClient. So, we need to be able to define a mock for this module. Let’s take a look at our module:

Note: This is an abbreviated look at the api client module, only including a handful of functions for finding/creating GitHub orgs.

Our module usesHTTPoison to enact web requests to the GitHub API.

Let’s define a behaviour that we can use in this module:

Now that we have our behaviour, we’ll tell our api client module to use it:

Now that our api client module implements a behavior, Mox can use that behavior to create a mock.

Defining a Mock

Note: In order for the test/support/mocks.ex file to be compiled by our application, we need to add the following to our mix file:

Now that our mock behavior is defined, we can teach our application to use the mock in the test environment, instead of the real api client.

Let’s say we have the following function in our GithubClient module that calls on GithubClient.ApiClient:

Note: This function calls on some ApiClientfunctions not included in our abbreviated ApiClient module or behavior. We’ve excluded their definitions for brevity.

Our GithubClient should be configured to grab the GithubClient.ApiClient in the dev and production environments, and the ApiClientBehaviourMock in the test environment.

In our config/dev.exs:

config :github_client, api_client: GithubClient.ApiClient

And in our config/test.exs

config :github_client, api_client: ApiClientBehaviourMock

We’ll teach the GithubClient module to grab the correctly configured api client from the application environment, instead of calling on ApiClient directly:

Now GithubClient will use the mock in the test environment, and we’re ready to define some expectations!

Setting Expectations Against The Mock

We already know that GithubClient will call on our ApiClientBehaviourMock when the tests run. So, we’ll need to set some expectations against this mock that will get our test passing. In order for this happy-path test to run, we’ll need to tell our mock to expect to receive certain input when find_or_create_org/3 is called, and to return some valid response.

Note: We won’t go into mocking the other functions called on our client here, instead we’ll just focus on this one example.

We expect ApiClientBehaviourMock to receive the find_or_create_organization/3 function with any args and return the tuple, {:ok, @org}. And that’s it!

Refactoring our Api Client to Mock HTTP Interactions

We’ll refactor GithubClient.ApiClient to abstract away the code that actually makes web requests. By separating out the concerns of GitHub API-specific logic from the responsibility of enacting HTTP requests, we end up with cleaner code that we can easily mock in our test suite.

First, we’ll define a new module responsible for using HTTPoison to make HTTP requests:

Next, we’ll remove HTTP-specific code from GithubClient.ApiClient and teach it to use our new adapter module instead.

Our api client no longer directly uses HTTPoison, it doesn’t know how to format request headers or request URLs and it doesn’t have to deal with any JSON processing. It is only in charge of kicking off requests to the appropriate GitHub API endpoint and formatting the response so that it can be consumed elsewhere in the application. Our refactored module is much cleaner and more adherent the the Single Responsibility Principle.

Mocking HTTP Interactions

Let’s say we have the following test:

When this test runs, GitHubClient.ApiClient will call GithubClient.HttpAdapter.get/1, which will in turn call on HTTPoison.Base’s get/2 function. So we need to define a mock for our adapter’s behavior and teach GithubClient.ApiClient to use the mock in the test environment, instead of calling on the adapter directly.

But wait, you might be thinking. We haven’t implemented a behaviour for our adapter module! Well, our adapter module usesHTTPoison.Base which implements its own behaviour. So, we don’t need to define one! Instead, we will create a mock for the HTTPoison.Base behaviour directly:

# test/support/mocks.exs  
Mox.defmock(HttpMock, for: HTTPoison.Base)

And we’ll configure our app to use our adapter module in dev and our mock adapter in the test environment:

# config/dev.exs
config :github_client, http_adatper: GithubClient.HttpAdapter
# config/test.exs
config :github_client, http_adapter: HttpMock

Lastly, we’ll teach GithubClient.ApiClient to grab the correct adapter from the application environment:

Now we’re ready to define expectations against our mock in our test example:

And that’s it! By identifying a division within our original api client, and separating out the GitHub API logic from the code that enacts HTTP requests, we were able to write clean, well-organized code that was easy to mock and test.

The next challenge we faced was configuring Mox to test code that executed asynchronously in a GenServer. Stay tuned for our upcoming post on using Mox with GenServers to learn more!


Thanks for reading! Want to work on a mission-driven team that loves building well-tested Elixir apps? We’re hiring!


Footer top

To learn more about Flatiron School, visit the website, follow us on Facebook and Twitter, and visit us at upcoming events near you.

Flatiron School is a proud member of the WeWork family. Check out our sister technology blogs WeWork Technology and Making Meetup.

Footer bottom

Flatiron Labs

We're the technology team at The Flatiron School (a WeWork…

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store