Stateful in-memory Repo fake (closed-world). Recommended default.
The state is the complete truth — if a record isn't in the store, it doesn't exist. This makes the adapter authoritative for all bare schema operations without needing a fallback function.
Implements DoubleDown.Contract.Dispatch.FakeHandler, so it can
be used by module name with Double.fake:
Usage with Double.fake
# Basic — all bare-schema reads work without fallback:
DoubleDown.Double.fake(DoubleDown.Repo, DoubleDown.Repo.InMemory)
# With seed data:
DoubleDown.Double.fake(DoubleDown.Repo, DoubleDown.Repo.InMemory,
[%User{id: 1, name: "Alice"}, %Post{id: 1, title: "Hello"}])
# Layer expects for failure simulation:
DoubleDown.Repo
|> DoubleDown.Double.fake(DoubleDown.Repo.InMemory)
|> DoubleDown.Double.expect(:insert, fn [changeset] ->
{:error, Ecto.Changeset.add_error(changeset, :email, "taken")}
end)ExMachina integration
setup do
DoubleDown.Double.fake(DoubleDown.Repo, DoubleDown.Repo.InMemory)
insert(:user, name: "Alice", email: "alice@example.com")
insert(:user, name: "Bob", email: "bob@example.com")
:ok
end
test "lists all users" do
assert [_, _] = MyApp.Repo.all(User)
end
test "finds user by email" do
assert %User{name: "Alice"} =
MyApp.Repo.get_by(User, email: "alice@example.com")
end
test "count users" do
assert 2 = MyApp.Repo.aggregate(User, :count, :id)
endAuthoritative operations (bare schema queryables)
| Category | Operations | Behaviour |
|---|---|---|
| Writes | insert, update, delete | Store in state |
| PK reads | get, get! | nil/raise on miss (no fallback) |
| Clause reads | get_by, get_by! | Scan and filter |
| Collection | all, one/one!, exists? | Scan state |
| Aggregates | aggregate | Compute from state |
| Bulk writes | insert_all, delete_all, update_all (set:) | Modify state |
| Transactions | transact, rollback | Delegate to sub-operations |
Ecto.Query fallback
Operations with Ecto.Query queryables (containing where,
join, select etc.) cannot be evaluated in-memory. These fall
through to the fallback function, or raise with a clear error:
DoubleDown.Double.fake(DoubleDown.Repo, DoubleDown.Repo.InMemory, [],
fallback_fn: fn
_contract, :all, [%Ecto.Query{}], _state -> []
end
)When to use which Repo fake
| Fake | State | Best for |
|---|---|---|
Repo.Stub | None | Fire-and-forget writes, canned reads |
Repo.InMemory | Complete store | All bare-schema reads; ExMachina factories |
Repo.OpenInMemory | Partial store | PK reads in state, fallback for rest |
Limitations
The following insert_all options are silently ignored because they
depend on database constraints that don't exist in memory:
on_conflict— upsert behaviour (:nothing,{:replace, fields}, etc.)conflict_target— which fields/index define the conflict
This is intentional: test code should match production code without
modification. Testing constraint behaviour requires a real database
via DataCase.
When returning: is a list of fields, insert_all returns maps
containing only those fields (matching Ecto adapter behaviour).
returning: true returns full structs.
Binary table name sources (e.g. "users") are not supported by
insert_all — use a fallback function or Double.expect for these.
See also
DoubleDown.Repo.OpenInMemory— open-world variant where absence is inconclusive, requiring a fallback for most reads.DoubleDown.Repo.Stub— stateless stub for fire-and-forget writes.
Summary
Functions
Stateful handler function with closed-world semantics.
Create a new InMemory state map. Same API as Repo.OpenInMemory.new/2.
Convert a list of structs into the nested state map for seeding.
Types
Functions
Stateful handler function with closed-world semantics.
Bare schema reads are authoritative — the state is the full truth.
Ecto.Query reads fall through to the fallback function.
Create a new InMemory state map. Same API as Repo.OpenInMemory.new/2.
Convert a list of structs into the nested state map for seeding.