DoubleDown.Log (double_down v0.47.2)

Copy Markdown View Source

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

expectation()

@type expectation() :: {:match, atom(), matcher(), pos_integer()} | {:reject, atom()}

matcher()

@type matcher() :: (tuple() -> boolean())

t()

@type t() :: %DoubleDown.Log{expectations: [expectation()]}

Functions

match(operation, matcher_fn, opts \\ [])

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 exactly n matching entries (default 1).

match(acc, operation, matcher_fn, opts)

@spec match(t(), atom(), matcher(), keyword()) :: t()

new()

@spec new() :: t()

Create an empty log expectation accumulator.

reject(operation)

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.

reject(acc, operation)

@spec reject(t(), atom()) :: t()

verify!(acc, contract, opts \\ [])

@spec verify!(t(), module(), keyword()) :: {:ok, list()}

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 — when true, every log entry for the contract must be matched by some matcher. Unmatched entries cause verification to fail. Default false (loose mode).

Returns {:ok, log} where log is the full dispatch log for the contract — useful in the REPL for inspecting what happened.