Unit Testing Phoenix Controllers with Mox

As the ecosystem for Elixir matures more and more, there are some libraries that seem particularly promising to me. One of them is Mox, a simple but powerful library for implementing mocks for predefined behaviours (note the British spelling!). José Valim started developing it only a few months ago, but I’ve already found it to be a very useful and flexible tool for writing tests.

Because Mox is so new, I haven’t been able to find many tutorials or guides for it. However, since it’s so easy to set up a basic mock, I thought it would be worthwhile to show how I’m using this library for a particular scenario: unit testing Phoenix controllers. I’ll first give an overview of Mox and why it’s appropriate for the task, and then I’ll walk through an actual implementation.

The Benefits of Mox

The philosophy behind Mox can be found in an article José wrote in 2010. A simple summary is that when it comes to dependency injection, mocks should not be created ad-hoc. Instead, they should be constrained by predefined behaviours. This helps enforce contracts between modules, and it also makes tests easier to maintain and understand.

Mox ties mock definitions to specific behaviours. It also requires that any stubbed functions are callbacks for that behaviour. But this library isn’t all about constraints–different implementations of mocks can be run concurrently, which is fantastic for tests. It’s also very easy to customize implementations in each test. Try to keep all of this in mind as you look over the walkthrough below.

Unit Testing a Phoenix Controller

All of the following assumes you’re using Elixir 1.4 and Phoenix 1.3.

  1. Run mix phx.new mox-guide
  2. Add {:mox, "~> 0.3.1"} to mix.exs, and run mix deps.get
  3. Add a configuration for a service to be used by the controller (which is coming up soon):
    config :mox_guide,
      user_service: MoxGuide.UserService
    

    Note that we’re not actually going to build this module, but luckily, we don’t need one for a clean compilation!

  4. Override that config in config/test.exs:
    config :mox_guide,
      user_service: MoxGuideWeb.UserServiceMock

    This is the module we’ll be using as a mock.

  5. Add the following to test_helper.exs:
    Mox.defmock(MoxGuideWeb.UserServiceMock, for: MoxGuideWeb.UserServiceBehaviour)
  6. Now add an endpoint to router.ex (inside the existing “/” scope):
    post "/register", UserController, :register

    Awesome!

Now, here are the three new files we need:

user_service_behaviour.ex

First of all, a module that defines the behaviour (I put mine in a directory reserved for service behaviours):


defmodule MoxGuideWeb.UserServiceBehaviour do
 @type user :: map
 @type error :: :invalid_params

 @doc """
 Registers a User with information about their credentials, preferences, etc.
 """
 @callback register_user(user_params :: map) :: {:ok, user} | {:error, error}
end

user_controller.ex

Notice that, for the sake of simplicity, we’re just responding with plain JSON instead of going through a view.


defmodule MoxGuideWeb.UserController do
 use MoxGuideWeb, :controller

 @user_service Application.get_env(:mox_guide, :user_service)

 def register(conn, %{"user" => _} = params) do
  with {:ok, user} <- @user_service.register_user(params) do 
   conn 
   |> put_status(:ok)
   |> json(user)
  else
   {:error, :missing_param} ->
    conn
    |> put_status(:unprocessable_entity)
    |> json(%{error: "invalid parameters"})
   {:error, :invalid_param} ->
    conn
    |> put_status(:unprocessable_entity)
    |> json(%{error: "invalid parameters"})
  end
 end
end

user_controller_test.ex


defmodule MoxGuideWeb.UserControllerTest do
 useMoxGuideWeb.ConnCase
  import Mox
  @service_mockMoxGuideWeb.UserServiceMock

  setup do
   base_params = %{user: %{name: "Chuck Testa"}}
   {:ok, params: base_params}
  end

  describe "REGISTER" do
   setup :verify_on_exit!

   test "valid parameters yield OK response and body", %{conn: conn, params: params} do
    expect(@service_mock, :register_user, fn _ -> {:ok, %{name: "Chuck Testa"}} end)
    conn = post conn, user_path(conn, :register), Map.to_list(params)
    assert json_response(conn, :ok) == %{"name" => "Chuck Testa"}
   end

   test "missing parameters yield 422 with error message", %{conn: conn, params: params} do
    expect(@service_mock, :register_user, 2, fn
      ^params ->
     {:ok, raise "We don't want this function to succeed!"}
      _->
     {:error, :invalid_param}
    end)

    error_resp = %{"error" => "invalid parameters"}
  
    # Missing embedded parameter
    user_params = put_in(params.user.name, nil)
    conn = post conn, user_path(conn, :register), user_params
    assert json_response(conn, :unprocessable_entity) == error_resp

    # Missing top-level parameter
    user_params = put_in(params.user, nil)
    conn = post conn, user_path(conn, :register), user_params
    assert json_response(conn, :unprocessable_entity) == error_resp
   end

   test "invalid name yields same response as missing name", %{conn: conn, params: params} do
    expect(@service_mock, :register_user, fn _ -> {:error, :invalid_param} end)
    conn2 = post conn, user_path(conn, :register), Map.to_list(params)
  
    expect(@service_mock, :register_user, fn _ -> {:error, :missing_param} end)
    conn = post conn, user_path(conn, :register), Map.to_list(params)
  
    assert response(conn, :unprocessable_entity) == response(conn2, :unprocessable_entity)
   end
  end
 end

Now just run mix test, and everything should pass.

How Mox is Working Here

See how easy the setup on Line 16 of user_controller_test is? Since we really want to test how the controller handles output from the service module, the input into the service module doesn’t matter. In this test, we know exactly what the controller will get back from the service, which lets us easily test out the response data in Line 20.

Line 24 shows how to expect a specific number of calls to register_user/2. You can also use stub/3 to circumvent any expectation validation. This expect is also a good example of how useful pattern matching is here. There’s one clause for a  “good” call to register_user/2 (which we don’t want to call), and one clause for all others. You could even stub a single implementation of register_user/2 at the top-level setup macro that just has one clause for each test scenario–that’s what I was doing for a while, but I decided it’s more readable to define expectations within each test.

You can also redefine expectations within a test, as on Line 50. While the exact placement of expectations is unfortunately not very strict, multiple definitions are useful if you want the same input (or in this case, any input with arity of 1) to have different outputs throughout the tests. Any calls to register_user/2 after the second definition will behave as if the first one never existed.

Going Forward

Once again, the best part of Mox is that it’s not only simple and flexible, but it also encourages best practices like explicitness and binding injected dependencies to contracts (i.e. behaviours). There are also a lot of nice tricks that aren’t utilized in the sample test here, such as replacing anonymous functions inside of expectations with references to other functions, like so:

expect(@service_mock, :register_user, &some_function/1)

This can boost readability and also supports the DRY principle. In addition, Mox has some features to support multi-process collaboration, if your situation requires it.

I don’t yet know if Mox would be quite as powerful when testing other types of modules like GenServers, but it’s been a pleasure to use for Phoenix controllers, and it’ll probably remain my go-to library for mocks in Elixir.

Edit (2018-04-11)

The original post never went into the actual implementation of service behaviours. That part isn’t hard, you just have to add the @behaviour attribute to a module. But having a separate behaviour-defining module for every service, just so that you can unit test controllers with Mox, is a little bit unappealing.

For that reason, I’m experimenting with a way to keep the behavior and implementation of a service module closer together in the code. My best solution so far is to use a macro that basically combines the @spec and @callback directives, which is used like so:


defmodule Example do
  use BehaviourExporter # implements `callback_spec/1`

  @doc “Whatever”
  callback_spec func(input :: int) :: :ok | :error
  def func(input), do: :ok
end

Here’s the implementation of BehaviourExporter:


defmodule BehaviourExporter do
  @doc "Requires and imports Behaviour Exporter so that `callback_spec/1` is available."
  defmacro __using__(_opts) do
    quote do
      require unquote(__MODULE__)
      import unquote(__MODULE__)
    end
  end

  @doc "Combines the @spec and @callback module attributes."
  defmacro callback_spec(term) do
    quote do
      @callback unquote(term)
      @spec unquote(term)
    end
  end
end

The benefit is that the service’s behavior and main implementation are always synchronized and there’s a single source of truth. Another benefit is that using Mox becomes a little simpler, since you don’t have to keep track of behaviour modules. The downside is that the implementation module doesn’t technically implement the behavior (Example can’t implement its own behaviour because of compilation dependencies). I haven’t found any real repercussions of this, but it does feel kind of hack-y. If you have any ideas to improve this pattern, please leave a comment below!

Conversation
  • Mike says:

    That would be the best article on controllers’ testing I’ve ever read. I hope people will practice this approach

  • Teerawat says:

    This is nice, thanks for posting :)

  • aquarhead says:

    Using `@impl` would be cleaner than the `callback_spec` macro, I think https://hexdocs.pm/elixir/Module.html#module-impl

  • Fernando Hamasaki de Amorim says:

    How to avoid compilation warnings in test envirionment saying that mocks are not available?

    For example:

    warning: function MoxGuideWeb.UserServiceMock.register_user/1 is undefined (module MoxGuideWeb.UserServiceMock is not available)
    lib/mox_guide_web/controllers/user_controller.ex:4

    • Aaron King Aaron King says:

      Hey Fernando,

      I can’t be positive without seeing your code, but it looks like `Mox.defmock(MoxGuideWeb.UserServiceMock, for: …)` is not getting called. You may want to check your compilation paths inside `mix.exs`, and write a log statement (use `IO.inspect`) right before you call `defmock`.

      After you fix that problem, you might encounter an error about a function not being defined for your mock. If that happens, you may need to run `mix deps.clean` (or delete the `build/` directory) and then re-compile your application. Not sure exactly why this is, but it has to do with cached definitions of Behaviours.

      Good luck, and let me know if this helps!

  • Comments are closed.