Elixir Test Mocking with Mox

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

This post was originally published on the Flatiron Labs blog, The Flatiron School Technology Team's blog. Check out more awesome Flatiron Labs posts here.

Why We Need Mox

In a recent post, we talked about the age-old question: How can you test code that relies on external services, like an API? We don’t want slow-running tests that make web requests to external services, potentially impacting things like API rate limits, production data and the overall reliability of our test suite.

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

First things first, We need to add the Mox dependency to the mix.exs file of our GithubClient app:

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

Mocking our Github Api Client

Defining a Behaviour

In order to use Mox to create a mock of a given module, that module must be an Elixir behaviour. Behaviours allow us to define a specific API that a set of modules must adhere to. We need to turn the module we want to create a mock out of into a behavior so that Mox can use the behavior to build the mock.

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

Telling Mox to define a mock for a given behavior is pretty straightforward. We’ll define our mock in test/support/mocks.exs

Note: In order for the test/support/mocks.exs 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

Let’s say we have the following test for the function above:

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

Defining a behavior for GithubClient.ApiClient allowed us to create a mock we could use when testing code that calls on this api client module. But what about testing the api client itself? After all, this module implements a fair amount of its own logic. So, how can we test the logic in our api client module, but still create a mock to handle the actual 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

Now that we have a separate GithubClient.HttpAdapter module that we can mock, we’re ready to take a look at some GithubClient.ApiClient tests.

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!

subscribe and never miss a post!

Blog Logo

Sophie DeBenedetto

comments powered by Disqus
comments powered by Disqus