Packages
double_down
0.46.0
0.69.0
0.68.0
0.66.0
0.65.0
0.64.1
0.64.0
0.63.3
0.63.2
0.63.1
0.63.0
0.62.1
0.61.0
0.60.4
0.60.3
0.60.2
0.60.1
0.60.0
0.59.0
0.58.0
0.57.0
0.56.1
0.56.0
0.55.0
0.54.0
0.53.0
0.52.3
0.52.2
0.52.1
0.52.0
0.51.0
0.50.1
0.50.0
0.49.0
0.48.1
0.48.0
0.47.2
0.47.1
0.47.0
0.46.3
0.46.2
0.46.1
0.46.0
0.45.0
0.44.0
0.43.0
0.42.0
0.41.1
0.41.0
0.40.0
0.39.0
0.38.0
0.37.2
0.37.0
0.35.0
0.34.0
0.33.0
0.32.0
0.31.1
0.31.0
0.30.1
0.30.0
0.29.0
0.28.1
0.28.0
0.27.0
0.26.0
0.24.0
Builds on the Mox pattern — generates behaviours and dispatch facades from `defcallback` declarations — and adds stateful test doubles powerful enough to test Ecto.Repo operations without a database.
Current section
Files
Jump to
Current section
Files
docs/dynamic.md
# 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)