Debugging and mocking third party services in Elixir with Mox
Tags: elixir, testing
Published: 2021-05-19 15:12:42


Almost every backend application uses them, third party services. Whether it's S3 for object storage, Segment for analytics or Firebase for push notifications. Those services are super helpful and do a lot of heavy-lifting, but in my experience usually do make debugging business logic on your local machine a pain, and testing them isn't easy either.

There are some great solutions available in Elixir. In this post I'm sharing the approach I have been using for the past few years using Elixir. I'll be going over defining behaviours and using them for local debugging during development as well as writing tests.

## Defining a behaviour

Interfaces should be familiar for anyone coming from typed object-oriented languages such as Java or TypeScript. Elixir's equivalent of this is called a [behaviour](https://hexdocs.pm/elixir/typespecs.html#behaviours). By using type-specs we can define a module's functions in a behaviour, which can be implemented by multiple modules, usually called "callbacks" in Elixir.

If you are unfamiliar with type-specs, I highly recommend [this comprehensive blog post](https://inquisitivedeveloper.com/lmw-elixir-69/) by [Kevin Peter](https://inquisitivedeveloper.com)

For this post's example we want to define a simple behaviour that can send a push notification to a user. We're going to write a behaviour module that will describe the function definitions we need to send push notifications.

In a behaviour, each `@callback` annotation defines the type-spec of a function that is required on any implementation of this behaviour. For our push notifications use-case the simple behaviour below will do; a single `send_push` function that takes a User struct as a recipient and a string as a title for the push notification.

```elixir
defmodule MyApp.Infrastructure.PushBehaviour do
  # Sends a push notification to the given User with the given title.
  # Returns :ok if the message was sent or an :error tuple with an error message.
  @callback send_push(recipient :: MyApp.User.t(), title :: String.t()) ::
              :ok | {:error, String.t()}
end
```

## Stub, Debug and Production implementations

Once we have determined what our behaviour looks like, we can start thinking about what our implementations may look like. I usually end up writing three implementations, one for each Mix environment.

- **Development**: A behavior that helps us _debug_ our business logic locally without depending on secrets or even an internet connection. In the case of push notifications we can simply log the user's name and the message they will be receiving to the Elixir `Logger`. For other use-cases, like object file storage, we can choose to write/modify/delete files on our local filesystem in a temporary folder instead of to for example Login.
- **Testing**: For testing you want to define a "_stub"_ that doesn't actually do anything with the data, and always acts as if anything you're trying to do is successful. You want to prevent calling your third party vendor hundreds of times while running tests. They may rate-limit you or it may incur costs. In those stubs you always want to return a succesful response, in our case that is `:ok`, acting as if the push notification was sent succesfully. We'll go over testing error responses from your behaviour later.
- **Production**: This will call the third party service, which may be a library, rest API or whatever. Entirely up to you.

Implementing a behaviour on a callback module is done by adding the `@behaviour` decorator as well as implementing all of its defined callbacks. Here are some example implementations for our Stub, Debug and Production implementation.

Our stub implementation will always return `:ok`, it's there mainly to not raise any errors while running tests or compiling your app.

```elixir

defmodule MyApp.Infrastructure.StubPush do
  @moduledoc """
  Do nothing. Always succeed.
  """

  @behaviour MyApp.Infrastructure.PushBehaviour

  def send_push(_user, _title) do
    :ok
  end
end
```

Our debug implementation will use `Logger` to print the recipient's username and the title of the push notification. This is useful when writing your application locally and trying out your code without having to connect to your third party service.

```elixir
defmodule MyApp.Infrastructure.DebugPush do
  @moduledoc """
  Print a push notification to our logger
  """

  @behaviour MyApp.Infrastructure.PushBehaviour

  require Logger

  def send_push(user, title) do
    Logger.info("Sending a notification to user @#{user.username} with title \"#{title}\"")
    :ok
  end
end
```

For a production example, I'm assuming we're using a fictional Firebase library for sending notifications. This can be any service or library for your use-case of course.

```elixir
defmodule MyApp.Infrastructure.FirebasePush do
  @moduledoc """
  Send a push notification through firebase
  """

  @behaviour MyApp.Infrastructure.PushBehaviour

  def send_push(user, title) do
    SomeFirebaseLibrary.some_send_function(user.token, title)
  end
end
```

## Mocking

The go-to library for working with mocks in Elixir is [Mox](https://github.com/dashbitco/mox), authored by José Valim. Follow the [instructions on their docs to have it installed](https://github.com/dashbitco/mox#installation) so we can jump straight into setting up testing for our application's push notification.

First we're going to head over to `test_helper.exs` in your project. Every Elixir project with tests will have this file generated by default.

We are going to add a mock with `Mox.defmock`. This generates a module based on the behaviour we pass it. It's important to define a "stub" as well. Mox doesn't know what to return for each function and will error when a function is called without having an implementation. `Mox.stub_with` will fill those functions with the always-succeeding callbacks we defined in our `StubPush` module.

```elixir
# In test/test_helper.exs

# This will generate a module named MyApp.Infrastructure.MockPush
Mox.defmock(MyApp.Infrastructure.MockPush, for: MyApp.Infrastructure.PushBehavior)
# This will define all callbacks
Mox.stub_with(MyApp.Infrastructure.MockPush, MyApp.Infrastructure.StubPush)

ExUnit.start()
```

We are now ready to start writing the tests for our push notification business logic. To prepare a test module for dealing with mox we need to

- add `import Mox` so we can use Mox's `expect` functions later on
- add `setup :verify_on_exit!` which will perform some checks about your mocks after a test has run.

```elixir
defmodule MyApp.PushTest do
  use ExUnit.Case, async: true

  # 1. Import Mox
  import Mox
  # 2. setup fixtures
  setup :verify_on_exit!

  # Here go your tests...
end
```

In our tests we want to assert two main things:

- Are our `send_push` functions called with the right parameters
- Does our application handle errors returned from `send_push`

In order to do this we have to overwrite our stub behavior with a function specific to said test case. Mox provides the `expect` function for this, which we imported earlier. `expect` changes the function body for that specific test. Other tests will still use the stubbed behavior. Let's start with asserting whether the right parameters were given to `send_push`.

[There's a cool blog-post by José Valim detailing the decisions that lead to the design of Mox. I'd recommend reading it if you want to dive a bit deeper.](https://dashbit.co/blog/mocks-and-explicit-contracts)

When verifying parameters, you usually want to depend on pattern matching in Mox's case. With the pin operator (`^`) we can verify whether `send_push` was called with the same `user` as we passed into `do_something_that_sends_push`. Mox will ensure that your test fails when an `expect` is never called or when there is no matching function clause.

```elixir
test "Succesfully sends push notification to right user" do
  # Tip: ExMachina is a great library that helps generate entities like this.
  user = insert(:user)

  MyApp.Infrastructure.MockPush
  # Check if the user on `send_push` is the same user as we passed thru our call below
  # If the user does not match, this will throw a MatchError
  |> expect(:send_push, fn ^user, _title -> :ok end)

  MyApp.do_something_that_sends_push(user)
end
```

We can also use `expect` to return an error for `send_push` and check whether our business logic handles this properly. Let's say we want our `do_something_that_sends_push` function to propagate the push error to its caller, its test will look something like this:

```elixir
test "Succesfully propagates errors from push service" do
  # Tip: ExMachina is a great library that helps generate entities like this.
  user = insert(:user)

  MyApp.Infrastructure.MockPush
  # Check if the user on `send_push` is the same user as we passed thru our call below
  # If the user does not match, this will throw a MatchError
  |> expect(:send_push, fn _user, _title -> {:error, "This user does not have push enabled"} end)

  assert MyApp.do_something_that_sends_push(user) == {:error, "This user does not have push enabled"}
end
```

## Tying it all together

These tests won't succeed yet. There is one last step before we can successfully try out our test and debug push implementations. We need to tell our application which implementation of our Push behavior it needs to call in certain environments. To do this we are going to use the `Application` module which comes with Elixir, and the config files automatically generated in your Elixir project:

```elixir
# In config/dev.exs we use our push notification logger
config :my_app, :push_api, MyApp.Infrastructure.DebugPush

# In config/test.exs we use our Mock as named in Mox.defmock
config :my_app, :push_api, MyApp.Infrastructure.MockPush

# In config/prod.exs we want to use our Firebase client
config :my_app, :push_api, MyApp.Infrastructure.FirebasePush
```

Now in any case where you need to send a push notification, you should get the currently configured push implementation from the Application's environment before you call it. You can wrap this in a module if you use the implementation a lot.

```elixir
# individually
Application.get_env(:my_app, :push_api).send_push(user, "title")

# wrapped in a module
defmodule MyApp.Infrastructure.Push do
  def send_push(user, title) do
    Application.get_env(:my_app, :push_api).send_push(user, title)
  end
end
```

You can now call `MyApp.Infrastructure.Push.send_push`, which will look up the PushBehaviour implementation in your Application's environment and call the defined module. Try it out by running `iex -S mix` and calling the function. For your dev environment it will log a message to the console!

## In conclusion

Defining a behaviour and different implementations

- Are useful when debugging your app's business logic without having to depend on a third party service on your local machine
- Can be used to stub your calls to third party vendors, preventing you from calling them hundreds of times during tests.
- Can be used together with `Mox` to assert whether your app behaves correctly with different responses from your third party vendors' APIs

Thanks for reading, feel free to hit me up on [twitter](https://twitter.com/nickvernij) if you have any questions or feedback.