Generates a module that implements a behaviour by forwarding every callback to an implementation module, or by generating no-op stubs.
Lets callers depend on a stable facade (MyApp.Mailer) while the implementation (MyApp.Mailer.Impl,
a Mox mock in tests, ...) is chosen by configuration.
Usage
# The contract. It must be compiled before the facade.
defmodule MyApp.Mailer.API do
@callback deliver(to :: String.t(), subject :: String.t()) :: {:ok, term()} | {:error, term()}
end
# The default implementation, found by the <facade>.Impl naming convention.
defmodule MyApp.Mailer.Impl do
@behaviour MyApp.Mailer.API
@impl true
def deliver(to, subject), do: MyApp.SmtpClient.send(to, subject)
end
# The facade: implements MyApp.Mailer.API by forwarding each call.
defmodule MyApp.Mailer do
use BoxTurtle.Runtime.DynamicInterface.ModuleDispatch, otp_app: :my_app
end
# Callers only know the facade.
defmodule MyApp.Accounts do
def send_welcome(user), do: MyApp.Mailer.deliver(user.email, "Welcome!")
endIn tests, point the facade at a Mox mock:
# test/test_helper.exs
Mox.defmock(MyApp.Mailer.Mock, for: MyApp.Mailer.API)
# config/test.exs
config :my_app, MyApp.Mailer, impl: MyApp.Mailer.Mock
# test/my_app/accounts_test.exs
defmodule MyApp.AccountsTest do
use ExUnit.Case, async: true
import Mox
setup :verify_on_exit!
test "send_welcome/1 mails the user" do
expect(MyApp.Mailer.Mock, :deliver, fn "ann@example.com", "Welcome!" -> {:ok, :sent} end)
assert {:ok, :sent} = MyApp.Accounts.send_welcome(%{email: "ann@example.com"})
end
endOptions
:behaviour- the behaviour to implement. Defaults to<using module>.API. It must be compiled before the using module.:impl- the default implementation module. Defaults to<using module>.Impl.:otp_app- when given,impl_module/0readsconfig :otp_app, <using module>, impl: moduleat runtime, falling back to:impl.:generate_stub- whentrue, every callback is defined to return:okinstead of forwarding. Defaults tofalse. Combine it withApplication.compile_env/3to stub a fire-and-forget interface out of some builds.
Patterns
Because impl_module/0 reads the configuration on every call, one facade supports:
- per-build implementations - set
impl:differently inconfig/dev.exs,test.exsandprod.exs; - runtime switching -
Application.put_env/3takes effect on the next call (failover, feature flags); - Mox - point
impl:atMox.defmock(MyApp.Mailer.Mock, for: MyApp.Mailer.API)in the test build, and useMox.stub_with/2to fall back to a real implementation; - A/B tests - make the configured implementation a router that implements the same behaviour and picks a variant per caller.
Worked examples of each, with fake implementations and the tests that use them, are in the
repository: test/support/examples/dispatch.ex and
test/runtime/dynamic_interface/dispatch_examples_test.exs.