defmodule Mockable do
require Logger
@moduledoc """
Documentation for `Mockable`.
"""
@doc """
Configures which module to use for a given mockable module.
If no implementation is provided, the mockable module itself will be used.
This is useful for testing your Mockable module.
```elixir
Mockable.use(Client)
```
Or to use a specific implementation:
```elixir
Mockable.use(Client, ClientMock)
```
This function stores the configuration in process memory so that it is compatible with async tests.
"""
def use(mockable, implementation \\ nil) do
implementation = implementation || mockable
Process.put({Mockable, mockable}, implementation)
end
defmacro __using__(_opts) do
quote do
@before_compile unquote(__MODULE__)
@behaviour __MODULE__
end
end
defmacro __before_compile__(env) do
module = env.module
module_string = module |> Atom.to_string() |> String.replace_prefix("Elixir.", "")
if Application.get_env(:mockable, module) do
callback_specs =
Module.get_attribute(module, :callback)
|> Enum.map(fn {:callback, spec, _} ->
case spec do
{:"::", _, [{name, _, args}, _return_type]} ->
{{name, length(args)}, spec}
end
end)
|> Map.new()
|> Map.take(Module.definitions_in(module, :def))
wrapped_functions =
for {{name, arity}, spec} <- callback_specs do
args = Macro.generate_arguments(arity, __MODULE__)
quote do
defoverridable [{unquote(name), unquote(arity)}]
@spec unquote(spec)
def unquote(name)(unquote_splicing(args)) do
implementation =
Process.get({Mockable, unquote(module)}) ||
Application.get_env(:mockable, unquote(module))
if implementation != unquote(module) do
Mockable.log_implementation_usage(
implementation,
unquote(name),
unquote(arity),
unquote(module_string)
)
apply(implementation, unquote(name), [unquote_splicing(args)])
else
Mockable.log_implementation_usage(
implementation,
unquote(name),
unquote(arity),
unquote(module_string)
)
super(unquote_splicing(args))
end
end
end
end
quote do
require Logger
(unquote_splicing(wrapped_functions))
end
end
end
if Application.compile_env(:mockable, :log, true) do
def log_implementation_usage(implementation, function_name, arity, module) do
Logger.debug("Using #{inspect(implementation)}.#{function_name}/#{arity} for #{module}")
end
else
def log_implementation_usage(_implementation, _function_name, _arity, _module), do: :ok
end
end