DoubleDown.DynamicFacade (double_down v0.63.2)

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

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

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

ensure_shimmed(module)

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

setup(module)

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

setup?(module)

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

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.