Current section
Files
Jump to
Current section
Files
lib/delx.ex
defmodule Delx do
@moduledoc """
An Elixir library to make function delegation testable.
## Usage
Let's say you have the following module.
iex> defmodule Greeter.StringGreeter do
...> def hello(name) do
...> "Hello, \#{name}!"
...> end
...> end
You can delegate functions calls to another module by importing the `Delx`
module and using the `defdel/2` macro. It has the same syntax and options as
Elixir's own `Kernel.defdelegate/2` macro.
iex> defmodule Greeter do
...> import Delx
...> defdel hello(name), to: Greeter.StringGreeter
...> end
iex> DelegatingModule.hello("Tobi")
"Hello, Tobi!"
## Testing
One great benefit of Delx is that you can test delegation without invoking
the actual implementation of the delegation target, thus eliminating all side
effects.
### With the Stub Delegator
Delx brings it's own stub delegator.
You can activate it for your test environment by putting the following line in
your `config/test.exs`:
config :delx, :stub, true
# or
config :delx, :delegator, Delx.Delegator.Stub
Then in your tests, you can assert whether delegation took place:
defmodule GreeterTest do
use ExUnit.Case
import Delx.TestAssertions
describe "hello/1" do
test "delegate to Greeter.StringGreeter" do
assert_delegate {Greeter, :hello, 1}, to: Greeter.StringGreeter
end
end
end
### With Mox
If you are using [Mox](https://hexdocs.pm/mox) in your application you have
another possibility to test delegates.
Add the mock for the `Delx.Delegator` behavior to your `test/test_helper.exs`:
Mox.defmock(Delx.Delegator.Mock, for: Delx.Delegator)
Then, in your `config/test.exs` you have to set the mock as delegator module.
config :delx, :delegator, Delx.Delegator.Mock
Please make sure not to use the `:stub` option and a `:delegator` option at
the same time as this may lead to unexpected behavior.
Now you are able to `expect` calls to delegated functions:
defmodule GreeterTest do
use ExUnit.Case
import Mox
setup :verify_on_exit!
describe "hello/1" do
test "delegate to Greeter.StringGreeter" do
expect(
Delx.Delegator.Mock,
:apply,
fn {Greeter, :hello},
{Greeter.StringGreeter, :hello},
["Tobi"] ->
:ok
end)
Greeter.hello("Tobi")
end
end
end
"""
@doc """
The module that is used to control delegation. Has to implement the
`Delx.Delegator` behavior.
"""
@spec __delegator__() :: module
def __delegator__ do
case Application.fetch_env(:delx, :stub) do
{:ok, true} -> Delx.Delegator.Stub
_ -> Application.get_env(:delx, :delegator, Delx.Delegator.Common)
end
end
@doc """
Defines a function that delegates to another module. Has the same API as
`Kernel.defdelegate/2`.
"""
defmacro defdel(funs, opts) do
funs = Macro.escape(funs, unquote: true)
quote bind_quoted: [funs: funs, opts: opts] do
target =
opts[:to] || raise ArgumentError, "expected to: to be given as argument"
for fun <- List.wrap(funs) do
{name, args, as, as_args} = Kernel.Utils.defdelegate(fun, opts)
@doc delegate_to: {target, as, :erlang.length(as_args)}
def unquote(name)(unquote_splicing(args)) do
Delx.__delegator__().apply(
{unquote(__MODULE__), unquote(name)},
{unquote(target), unquote(as)},
unquote(args)
)
end
end
end
end
end