Dynamic dispatch facades for existing modules.
Enables Mimic-style bytecode interception — replace any module with
a dispatch shim at test time, then use the full DoubleDown.Double
API (expects, stubs, fakes, stateful responders, passthrough) without
defining a contract or facade.
Setup
Call setup/1 in test/test_helper.exs before ExUnit.start():
DoubleDown.DynamicFacade.setup(MyApp.EctoRepo)
DoubleDown.DynamicFacade.setup(SomeThirdPartyModule)
ExUnit.start()Usage in tests
setup do
DoubleDown.Double.fallback(MyApp.EctoRepo, DoubleDown.Repo.OpenInMemory)
:ok
end
test "insert then get" do
{:ok, user} = MyApp.EctoRepo.insert(User.changeset(%{name: "Alice"}))
assert ^user = MyApp.EctoRepo.get(User, user.id)
endTests that don't install a handler get the original module's behaviour — zero impact on unrelated tests.
Struct modules
If the original module defines a struct (defstruct), the shim
preserves full struct support:
%Module{}literal syntax works at compile time in tests__info__(:struct)returns correct field metadata@enforce_keysand default values are preserved__struct__/0and__struct__/1calls route throughdispatch/3, soDouble.fallback/Double.expecthandlers can intercept struct construction at runtime
Behaviour and macro modules
@behaviourdeclarations are copied from the original module to the shim, so behaviour-based dispatch and compliance checks work correctly.- Macros (
defmacro) are proxied viadefmacrowrappers that delegate to the original implementation. Macros expand at compile time so they always use the original — they cannot be intercepted byDoublehandlers.
Constraints
- Call
setup/1before tests start (intest_helper.exs). Bytecode replacement is VM-global; calling it during async tests may cause flaky behaviour. - Cannot set up dynamic facades for DoubleDown contracts (use
DoubleDown.ContractFacadeinstead), DoubleDown internals, NimbleOwnership, or Erlang/OTP modules.
See also
DoubleDown.ContractFacade— dispatch facades fordefcallbackcontracts (typed, LSP-friendly, recommended for new code).DoubleDown.BehaviourFacade— dispatch facades for vanilla@behaviourmodules (typed, but no pre_dispatch or combined contract + facade).
Summary
Functions
Dispatch a call through the dynamic facade.
Set up a dynamic dispatch facade for a module.
Check whether a module has been set up for dynamic dispatch.
Functions
Dispatch a call through the dynamic facade.
Called by generated shims. Checks NimbleOwnership for a test
handler, falls back to the original module (Module.__dd_original__).
@spec setup(module()) :: :ok
Set up a dynamic dispatch facade for a module.
Copies the original module to a backup (Module.__dd_original__)
and replaces it with a shim that dispatches through
DoubleDown.DynamicFacade.dispatch/3.
Call this in test/test_helper.exs before ExUnit.start().
Bytecode replacement is VM-global — calling during async tests may
cause flaky behaviour.
After setup, use the full DoubleDown.Double API:
DoubleDown.Double.fallback(MyModule, handler)
DoubleDown.Double.expect(MyModule, :op, fn [args] -> result end)Tests that don't install a handler get the original module's behaviour automatically.
Check whether a module has been set up for dynamic dispatch.