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()Setup is lazy: modules are registered for potential shimming but
no bytecode manipulation happens until a test installs a handler via
DoubleDown.Double.fallback/2, expect/3, or stub/2. This means
a test run that never exercises a module pays zero shimming cost.
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.
Ensure a lazily-registered module is fully shimmed.
Register a module for lazy dynamic dispatch.
Check whether a module has been registered 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 ensure_shimmed(module()) :: :ok
Ensure a lazily-registered module is fully shimmed.
If the module is in the lazy set, performs the bytecode manipulation (rename original + create dispatch shim) and moves it to the shimmed set. If the module is already shimmed or not registered, this is a no-op.
Called by DoubleDown.Testing.set_meta/2 when a handler is first
installed, and as a safety net in dispatch/3.
@spec setup(module()) :: :ok
Register a module for lazy dynamic dispatch.
The module is registered for potential shimming but no bytecode
manipulation happens yet. The actual shim (rename original + create
dispatch shim) fires lazily when a test first installs a handler via
DoubleDown.Double.fallback/2, expect/3, or stub/2.
Call this in test/test_helper.exs before ExUnit.start().
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 — and never pay the shimming cost.
Check whether a module has been registered for dynamic dispatch.
Returns true if the module is either lazily registered (not yet
shimmed) or fully shimmed.