DoubleDown.DynamicFacade (double_down v0.56.0)

Copy Markdown View Source

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)
end

Tests 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_keys and default values are preserved
  • __struct__/0 and __struct__/1 calls route through dispatch/3, so Double.fallback / Double.expect handlers can intercept struct construction at runtime

Behaviour and macro modules

  • @behaviour declarations are copied from the original module to the shim, so behaviour-based dispatch and compliance checks work correctly.
  • Macros (defmacro) are proxied via defmacro wrappers that delegate to the original implementation. Macros expand at compile time so they always use the original — they cannot be intercepted by Double handlers.

Constraints

  • Call setup/1 before tests start (in test_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.ContractFacade instead), DoubleDown internals, NimbleOwnership, or Erlang/OTP modules.

See also

  • DoubleDown.ContractFacade — dispatch facades for defcallback contracts (typed, LSP-friendly, recommended for new code).
  • DoubleDown.BehaviourFacade — dispatch facades for vanilla @behaviour modules (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(module, operation, args)

@spec dispatch(module(), atom(), [term()]) :: term()

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__).

setup(module)

@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.

setup?(module)

@spec setup?(module()) :: boolean()

Check whether a module has been set up for dynamic dispatch.