Phoenix.Sync.Sandbox (Phoenix.Sync v0.6.1)
View SourceIntegration between Ecto.Adapters.SQL.Sandbox and Electric that produces
replication events from Ecto operations within a sandboxed connection.
In normal operation Electric creates and consumes a logical replication
slot on your Postgres database. This makes testing difficult because this
replication stream is stateful and will not emit events when testing using
Ecto.Adapters.SQL.Sandbox (which aborts all operations before they could
appear in the replication stream).
Phoenix.Sync.Sandbox uses a custom Ecto.Adapter that intercepts writes
within a sandboxed connection and emits change events to a per-test
replication stack.
Integration
Step 1
In your config/test.exs file, set the mode to :sandbox:
config :phoenix_sync,
env: config_env(),
mode: :sandboxStep 2
Replace your Ecto.Repo's adapter with our sandbox adapter macro:
# before
defmodule MyApp.Repo do
use Ecto.Repo,
otp_app: :my_app,
adapter: Ecto.Adapters.Postgres
end
# after
defmodule MyApp.Repo do
use Phoenix.Sync.Sandbox.Postgres
use Ecto.Repo,
otp_app: :my_app,
adapter: Phoenix.Sync.Sandbox.Postgres.adapter()
endThis macro will configure the repo with Ecto.Adapters.Postgres in dev
and prod environments but enable intercepting db writes in tests.
Step 3
In your test file, after your Ecto.Adapters.SQL.Sandbox.checkout(Repo)
setup call, start a sandbox stack for your repo:
# before
setup do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo)
end
# after
setup do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo)
# start our sandbox replication stack
Phoenix.Sync.Sandbox.start!(Repo)
endOr if you're using Ecto.Adapters.SQL.Sandbox.start_owner!/2:
# before
setup do
pid = Ecto.Adapters.SQL.Sandbox.start_owner!(Repo)
on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
end
# after
setup do
pid = Ecto.Adapters.SQL.Sandbox.start_owner!(Repo)
on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
# start our sandbox replication stack
Phoenix.Sync.Sandbox.start!(Repo, pid)
endNow in your tests, inserting via the configured repo will emit change messages for sandboxed writes, exactly as if you were reading from the Postgres replication stream.
Collaborating processes
Allowances
The Phoenix.Sync.Sandbox uses the same ownership model as
Ecto.Adapters.SQL.Sandbox -- and processes that automatically inherit
access to the sandboxed connection will also register themselves to the
test electric stack.
test "tasks have access" do
start_supervised!({Task, fn ->
# this will succeed and broadcast a change event on the test's
# sandbox replication stream
Repo.insert!(%MyApp.Task{title: "Test Task"})
end})
endHowever processes started outside of the test process tree that need to be
explicitly granted access to the sandboxed
connection
will also need to be explicitly registered to the current test's
replication stream using Phoenix.Sync.Sandbox.allow/3. This calls
Ecto.Adapters.SQL.Sandbox.allow/3 and also registers the allow pid
against the current process's replication stack.
So where you would normally need to call
Ecto.Adapters.SQL.Sandbox.allow/3 simply call
Phoenix.Sync.Sandbox.allow/3 instead:
test "calls worker that runs a query" do
allow = Process.whereis(MyApp.Worker)
Phoenix.Sync.Sandbox.allow(Repo, self(), allow)
GenServer.call(MyApp.Worker, :run_query)
endShared mode
If you're configuring your Ecto sandbox in shared mode, you also need to configure
the Phoenix.Sync.Sandbox to use shared mode by passing shared: true to Phoenix.Sync.Sandbox.start!/3.
# before
setup(tags) do
pid = Ecto.Adapters.SQL.Sandbox.start_owner!(Repo, shared: not tags[:async])
on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
end
# after
setup(tags) do
pid = Ecto.Adapters.SQL.Sandbox.start_owner!(Repo, shared: not tags[:async])
on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
Phoenix.Sync.Sandbox.start!(Repo, pid, shared: not tags[:async])
endIntegrations
Oban
Because Phoenix.Sync.Sandbox.Postgres.adapter/0 configures your
Ecto.Repo to use a non-standard adapter module it causes problems
with Oban's migration system.
If you see an error like:
** (KeyError) key :migrator not found in: [
# your repo config...
]then the solution is to force Oban to use its Postgres migrator in your config/test.exs:
config :my_app, MyApp.Repo,
# base config...
migrator: Oban.Migrations.PostgresLimitations
The sandbox adapter will intercept the following functions:
Ecto.Repo.delete_all/2Ecto.Repo.update_all/3Ecto.Repo.delete/2Ecto.Repo.delete!/2Ecto.Repo.insert/2Ecto.Repo.insert!/2Ecto.Repo.insert_all/3Ecto.Repo.update/2Ecto.Repo.update!/2
It does this by potentially re-writing the SQL for the query. Because of this there will be some more complex queries that will fail. Please raise an issue if you hit problems.
Summary
Types
@type start_opts() :: [{:shared, boolean()}]
Functions
@spec allow(Ecto.Repo.t(), pid(), pid() | GenServer.name(), keyword()) :: :ok | no_return()
Allows the process name_or_pid to access the sandboxed transaction.
This is a wrapper around Ecto.Adapters.SQL.Sandbox.allow/3 that also
connects the given process to the active sync instance.
You should use it instead of Ecto.Adapters.SQL.Sandbox.allow/3 for tests
that are using the sandbox replication stack.
opts is passed to Ecto.Adapters.SQL.Sandbox.allow/3.
@spec client() :: {:ok, Electric.Client.t()} | {:error, String.t()}
Retrieve a client configured for the current sandbox stack.
Example:
{:ok, client} = Phoenix.Sync.Sandbox.client()
Electric.Client.stream(client, Todo, replica: :full),
@spec client!() :: Electric.Client.t() | no_return()
As per client/0 but raises if there is no stack configured for the
current process.
@spec start!(Ecto.Repo.t()) :: :ok | no_return()
See start!/3.
@spec start!(Ecto.Repo.t(), pid() | start_opts()) :: :ok | no_return()
See start!/3.
@spec start!(Ecto.Repo.t(), pid(), start_opts()) :: :ok | no_return()
Start a sandbox instance for repo linked to the given owner process.
Call this after your Ecto.Adapters.SQL.Sandbox.start_owner!/2 or
Ecto.Adapters.SQL.Sandbox.checkout/2 call.
setup do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo)
Phoenix.Sync.Sandbox.start!(Repo)
endOptions
shared(default:false) - start the sandbox in shared mode. Only enable shared mode if your repo sandbox is also in shared mode.tags- pass the test tags in order to generate a stack identifier based on the current test context