lib/moxable.ex

defmodule Moxable do
  @moduledoc """
  Moxable is a simplified method for working with Mox in an Elixir project.

  ## Setup

  Add `use Moxable` to any module you want to use Mox with in testing and define a behaviour 
  for your module. `use Mockable` takes three options:

  - `{:behaviour, behaviour_name}`: override the default behaviour name (by default Moxable 
    will use `<YOUR_MODULE_NAME>.Behaviour`)
  - {:behaviours, list_of_behaviours}: a list of behaviour modules to be used
  - `{:dev_impl, dev_module}`: override the module that will be used in the `:dev` context

  ## Usage

  Anywhere you will be calling or `expect`ing mocked functions of your Moxable'd module, 
  `use` it to define a dynamic alias to the real, mocked or dev version of the module based 
  on the current context. `:as` may be supplied as an option to override the alias name.
  """
  defmacro __using__(opts) do
    quote do
      module = Module.split(__MODULE__)

      behaviours =
        cond do
          Keyword.has_key?(unquote(opts), :behaviour) -> [Keyword.fetch!(unquote(opts), :behaviour)]
          Keyword.has_key?(unquote(opts), :behaviours) -> Keyword.fetch!(unquote(opts), :behaviours)
          true -> [(module ++ ["Behaviour"]) |> Module.concat()]
        end

      for behaviour <- behaviours do
        @behaviour behaviour
      end

      @mock_for (["Moxable"] ++ module) |> Module.concat()

      if Mix.env() == :test do
        Mox.defmock(@mock_for, for: behaviours)
      end

      @default_module_alias Module.concat([List.last(module)])

      @implementation (case Mix.env() do
                         :test -> @mock_for
                         :dev -> Keyword.get(unquote(opts), :dev_impl, __MODULE__)
                         _ -> __MODULE__
                       end)

      defmacro __using__(opts) do
        quote do
          alias unquote(@implementation),
            as: unquote(Keyword.get(opts, :as, @default_module_alias))
        end
      end
    end
  end
end