Packages
double_down
0.52.2
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/logging.md
# Logging
[< Dynamic Facades](dynamic.md) | [Up: README](../README.md) | [Process Sharing >](process-sharing.md)
## Dispatch logging
Record every call that crosses a contract boundary, then assert on
the sequence:
```elixir
setup do
MyApp.Todos
|> DoubleDown.Double.stub(:get_todo, fn [id] -> {:ok, %Todo{id: id}} end)
DoubleDown.Testing.enable_log(MyApp.Todos)
:ok
end
test "logs dispatch calls" do
MyApp.Todos.get_todo("42")
assert [{MyApp.Todos, :get_todo, ["42"], {:ok, %Todo{id: "42"}}}] =
DoubleDown.Testing.get_log(MyApp.Todos)
end
```
The log captures `{contract, operation, args, result}` tuples in
dispatch order. Enable logging before making calls; `get_log/1`
returns the full sequence.
## Log matcher (structured log assertions)
`DoubleDown.Log` provides structured expectations against the dispatch
log. Unlike `get_log/1` + manual assertions, it supports ordered
matching, counting, reject expectations, and strict mode.
This is particularly valuable with fakes like `Repo.InMemory` that do
real computation (changeset validation, PK autogeneration, timestamps)
— matching on results in the log is a meaningful assertion, not a
tautology.
### Basic usage
```elixir
DoubleDown.Testing.enable_log(MyApp.Todos)
# ... set up double and dispatch ...
DoubleDown.Log.match(:create_todo, fn
{_, _, [params], {:ok, %Todo{id: id}}} when is_binary(id) -> true
end)
|> DoubleDown.Log.reject(:delete_todo)
|> DoubleDown.Log.verify!(MyApp.Todos)
```
Matcher functions only need positive clauses — `FunctionClauseError`
is caught and treated as "didn't match". No `_ -> false` catch-all
needed, though returning `false` explicitly can be useful for
excluding specific values that are hard to exclude with pattern
matching alone.
### Counting occurrences
```elixir
DoubleDown.Log.match(:insert, fn
{_, _, [%Changeset{data: %Discrepancy{}}], {:ok, _}} -> true
end, times: 3)
|> DoubleDown.Log.verify!(DoubleDown.Repo)
```
### Strict mode
By default, extra log entries between matchers are ignored (loose
mode). Strict mode requires every log entry to be matched:
```elixir
DoubleDown.Log.match(:insert, fn _ -> true end)
|> DoubleDown.Log.match(:update, fn _ -> true end)
|> DoubleDown.Log.verify!(MyContract, strict: true)
```
### Using with DoubleDown.Double
Double and Log serve complementary roles — Double for fail-fast
validation and producing return values, Log for after-the-fact
result inspection:
```elixir
# Set up double
DoubleDown.Double.expect(MyContract, :create, fn [p] -> {:ok, struct!(Thing, p)} end)
DoubleDown.Testing.enable_log(MyContract)
# Run code under test
MyModule.do_work(params)
# Verify expectations consumed
DoubleDown.Double.verify!()
# Verify log entries match expected patterns
DoubleDown.Log.match(:create, fn
{_, _, _, {:ok, %Thing{}}} -> true
end)
|> DoubleDown.Log.verify!(MyContract)
```
---
[< Dynamic Facades](dynamic.md) | [Up: README](../README.md) | [Process Sharing >](process-sharing.md)