Log-based expectation matcher for DoubleDown dispatch logs.
Declares expectations against the dispatch log after execution.
Matches on the full {contract, operation, args, result} tuple —
including results, which is meaningful because DoubleDown handlers
(especially Repo.Stub) do real computation (changeset validation,
PK autogeneration, timestamps).
Basic usage
DoubleDown.Log.match(:insert, fn
{_, _, [%Changeset{data: %Thing{}}], {:ok, %Thing{}}} -> true
end)
|> DoubleDown.Log.reject(:delete)
|> DoubleDown.Log.verify!(MyContract)Matcher functions only need the positive matching clauses —
FunctionClauseError is caught and interpreted as "didn't match".
No need for a _ -> false catch-all, though returning false
explicitly can be useful for excluding specific values that are
hard to exclude with pattern matching alone.
Matching on results
Unlike Mox/Mimic where asserting on return values would be circular (you wrote the stub), DoubleDown handlers do real computation. Matching on results is a meaningful assertion:
DoubleDown.Log.match(:insert, fn
{_, _, [%Changeset{data: %Thing{}}],
{:ok, %Thing{id: id}}} when is_binary(id) -> true
end)
|> DoubleDown.Log.verify!(RepoContract)Counting occurrences
DoubleDown.Log.match(:insert, fn
{_, _, [%Changeset{data: %Discrepancy{}}], {:ok, _}} -> true
end, times: 3)
|> DoubleDown.Log.verify!(DoubleDown.Repo)Multi-contract
Build separate matcher chains and verify each against its contract:
todos_log =
DoubleDown.Log.match(:create_todo, fn {_, _, _, {:ok, _}} -> true end)
repo_log =
DoubleDown.Log.match(:insert, fn {_, _, _, {:ok, _}} -> true end)
DoubleDown.Log.verify!(todos_log, MyApp.Todos)
DoubleDown.Log.verify!(repo_log, DoubleDown.Repo)Matching modes
Loose (default)
Matchers must be satisfied in order within each operation, but other log entries are allowed between them. Different operations are matched independently (no cross-operation ordering):
DoubleDown.Log.match(:insert, matcher)
|> DoubleDown.Log.match(:update, matcher)
|> DoubleDown.Log.verify!(MyContract)
# Passes if log contains an insert and an update,
# regardless of other entries or relative order.Strict
Every log entry for the contract must be matched. No unmatched entries allowed:
DoubleDown.Log.match(:insert, matcher)
|> DoubleDown.Log.match(:update, matcher)
|> DoubleDown.Log.verify!(MyContract, strict: true)Relationship to existing APIs
Built on DoubleDown.Testing.get_log/1. Completely decoupled from
handler choice — works with Repo.Stub, Repo.OpenInMemory,
set_fn_handler, set_stateful_handler, or DoubleDown.Double.
Can be used alongside DoubleDown.Double — Handler for fail-fast
validation and producing return values, Log for after-the-fact
result inspection.
Summary
Functions
Add a match expectation for an operation.
Create an empty log expectation accumulator.
Add a reject expectation for an operation.
Verify all expectations against the dispatch log for a contract.
Types
@type expectation() :: {:match, atom(), matcher(), pos_integer()} | {:reject, atom()}
@type t() :: %DoubleDown.Log{expectations: [expectation()]}
Functions
Add a match expectation for an operation.
The matcher function receives the full log tuple
{contract, operation, args, result} and should return a truthy
value for entries that match. Write only the clauses that should
match — FunctionClauseError is caught and treated as "didn't
match".
The accumulator argument is optional — when omitted, a fresh
accumulator is created via new/0.
Options
:times— require exactlynmatching entries (default 1).
@spec new() :: t()
Create an empty log expectation accumulator.
Add a reject expectation for an operation.
Verification will fail if the operation appears anywhere in the log for the contract.
The accumulator argument is optional — when omitted, a fresh
accumulator is created via new/0.
Verify all expectations against the dispatch log for a contract.
Reads the dispatch log via DoubleDown.Testing.get_log/1 and
checks that all match expectations are satisfied and all reject
expectations hold.
Options
:strict— whentrue, every log entry for the contract must be matched by some matcher. Unmatched entries cause verification to fail. Defaultfalse(loose mode).
Returns {:ok, log} where log is the full dispatch log for the
contract — useful in the REPL for inspecting what happened.