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 using the `Delx` module and calling the `defdel/2` macro in the module body. It has the same API as Elixir's own `Kernel.defdelegate/2` macro. iex> defmodule Greeter do ...> use Delx, otp_app: :greeter ...> defdel hello(name), to: Greeter.StringGreeter ...> end iex> Greeter.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. ### Built-In Assertions Delx brings it's own test assertions. All you need to do is to activate delegation stubbing for your test environment by putting the following line in your `config/test.exs`: config :greeter, Delx, stub: true Then in your tests, you can import `Delx.TestAssertions` and use the `Delx.TestAssertions.assert_delegate/2` and `Delx.TestAssertions.refute_delegate/2` assertions. 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 Note that once you activate stubbing all delegated functions do not return anymore but instead raise the `Delx.StubbedDelegationError`. If you really want to call the original implementation, you have to avoid any calls of delegated functions. ### With Mox If you are using [Mox](https://hexdocs.pm/mox) in your application you have another possibility to test delegated functions. Register a mock for the `Delx.Delegator` behavior to your `test/test_helper.exs` (or wherever you define your mocks): Mox.defmock(Delx.Delegator.Mock, for: Delx.Delegator) Then, in your `config/test.exs` you have to set the mock as delegator module for your app. config :my_app, 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 For more information on how to implement your own delegator, refer to the docs of the `Delx.Delegator` behavior. """ defmacro __using__(opts) do quote bind_quoted: [opts: opts] do otp_app = opts[:otp_app] || raise ArgumentError, "expected otp_app: to be given as argument" @doc false @spec __delegator__() :: module def __delegator__ do config = Application.get_env(unquote(otp_app), Delx, []) case Keyword.fetch(config, :stub) do {:ok, true} -> Delx.Delegator.Stub _ -> Keyword.get(config, :delegator, Delx.Delegator.Common) end end import Delx.Defdel end end end