# Dynamic Facades
[< Testing](testing.md) | [Up: README](../README.md) | [Logging >](logging.md)
Dynamic facades enable Mimic-style bytecode interception — replace
any module with a dispatch shim at test time, then use the full
`DoubleDown.Double` API without defining an explicit contract or
facade. The shimmed module becomes both the **contract** (the name
used in test double setup) and the **facade** (what callers use).
## When to use dynamic facades
| Scenario | Approach |
|----------|----------|
| New code, long-term boundary | Contract-based (`defcallback` + `DoubleDown.ContractFacade`) |
| Existing `@behaviour` you don't control | `DoubleDown.BehaviourFacade` |
| Legacy code without contracts or behaviours | **Dynamic facade** |
| Third-party modules with no behaviour | **Dynamic facade** |
| Quick prototyping | **Dynamic facade**, graduate to contract-based later |
Dynamic facades trade compile-time safety (typespecs, LSP docs,
spec mismatch detection) for zero-ceremony setup. Both approaches
use the same dispatch and Double infrastructure — they coexist in
the same test suite.
## Setup
Call `DynamicFacade.setup/1` in `test/test_helper.exs` **before**
`ExUnit.start()`:
```elixir
# test/test_helper.exs
DoubleDown.DynamicFacade.setup(MyApp.EctoRepo)
DoubleDown.DynamicFacade.setup(SomeThirdPartyClient)
ExUnit.start()
{:ok, _} = DoubleDown.Testing.start()
```
`setup/1` copies the original module to a backup
(`Module.__dd_original__`) and replaces it with a dispatch shim.
The original module name becomes the implicit contract — use it
as the first argument to all `Double` API calls:
```elixir
# MyApp.EctoRepo is the contract — same module callers use
DoubleDown.Double.fake(MyApp.EctoRepo, DoubleDown.Repo.InMemory)
DoubleDown.Double.stub(SomeThirdPartyClient, fn :fetch, [id] -> {:ok, id} end)
```
The shim checks NimbleOwnership for test handlers, falling back to
the original implementation when none are installed. Bytecode
replacement is VM-global — it must happen before any tests run.
Tests that don't install a handler get the original module's
behaviour automatically.
## Using Double APIs
After setup, the full `DoubleDown.Double` API works with the
dynamic module — expects, stubs, fakes, passthrough, stateful
responders, cross-contract state access, dispatch logging:
```elixir
setup do
# Stateful fake
DoubleDown.Double.fake(MyApp.EctoRepo, DoubleDown.Repo.InMemory)
: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
```
### Stubs
```elixir
# Function stub
DoubleDown.Double.stub(SomeClient, fn
:fetch, [id] -> {:ok, %{id: id}}
:list, [] -> []
end)
# Per-operation stub
DoubleDown.Double.stub(SomeClient, :fetch, fn [id] -> {:ok, %{id: id}} end)
```
### Expects
```elixir
DoubleDown.Double.expect(SomeClient, :fetch, fn [_] -> {:error, :timeout} end)
# With passthrough — delegates to the original module
DoubleDown.Double.expect(SomeClient, :fetch, :passthrough)
# Stateful expect (requires a fake)
DoubleDown.Double.expect(SomeClient, :fetch, fn [id], state ->
{Map.get(state, id), state}
end)
```
### Override one operation, delegate the rest
Use `Double.dynamic/1` to set up the original module as the
fallback, then layer expects on top:
```elixir
SomeClient
|> DoubleDown.Double.dynamic()
|> DoubleDown.Double.expect(:fetch, fn [_] -> {:error, :not_found} end)
# fetch is overridden, all other functions delegate to the original
```
## Passthrough to original
When no test handler is installed, the dynamic shim automatically
falls back to the original module. This means unrelated tests are
completely unaffected.
When a handler IS installed, `Double.passthrough()` and
`:passthrough` expects delegate to the fallback (fake, stub, or
module fake) — not directly to the original. To delegate to the
original explicitly, use `Double.dynamic/1`:
```elixir
SomeClient
|> DoubleDown.Double.dynamic()
|> DoubleDown.Double.expect(:fetch, :passthrough)
```
## Cross-contract state access
Dynamic facades participate in cross-contract state access. A
4-arity handler on a dynamic module can read state from
contract-based facades, and vice versa:
```elixir
# Contract-based Repo with InMemory
DoubleDown.Double.fake(DoubleDown.Repo, DoubleDown.Repo.InMemory)
# Dynamic module reads Repo state
DoubleDown.Double.fake(MyApp.Legacy,
fn :check_user, [id], state, all_states ->
repo_state = Map.get(all_states, DoubleDown.Repo, %{})
users = repo_state |> Map.get(User, %{}) |> Map.values()
{Enum.any?(users, &(&1.id == id)), state}
end,
%{}
)
```
## Guardrails
`DynamicFacade.setup/1` refuses to set up facades for:
- **DoubleDown contract modules** — use `DoubleDown.ContractFacade` instead
- **DoubleDown internal modules** — would break the dispatch machinery
- **NimbleOwnership** — required by dispatch
- **Erlang/OTP modules** — would be catastrophic
## Comparison of facade types
See [Choosing a facade type](getting-started.md#choosing-a-facade-type)
for a full feature comparison table across `ContractFacade`,
`BehaviourFacade`, and `DynamicFacade`.
## Migration path
Start with dynamic facades for quick wins, then graduate to
typed facades for boundaries you want to keep long-term:
1. `DynamicFacade.setup(MyModule)` in test_helper.exs
2. Write tests using Double APIs
3. When the boundary stabilises, choose your facade type:
- If the module already defines `@callback` declarations,
use `DoubleDown.BehaviourFacade`
- Otherwise, define a `defcallback` contract and use
`DoubleDown.ContractFacade`
4. Remove the `DynamicFacade.setup` call
---
[< Testing](testing.md) | [Up: README](../README.md) | [Logging >](logging.md)