Combo.Ecto.SQL.Sandbox (combo_ecto v0.1.0)
View SourceA plug to allow concurrent, transactional acceptance tests with
Ecto.Adapters.SQL.Sandbox.
Requirements
PostgreSQL is required.
Usage
This plug should only be used during tests.
First, set a flag to enable it in config/test.exs:
config :demo, sql_sandbox: trueAnd, use the flag to conditionally add the plug to lib/demo/web/endpoint.ex:
if Application.compile_env(:demo, :sql_sandbox) do
plug Combo.Ecto.SQL.Sandbox
endIt's important that this is placed before the line of plug Demo.Web.Router,
or any other plugs that may access the database.
Then, within an acceptance test, checkout a sandboxed connection as before.
Use metadata_for/2 helper to get the session metadata to that will allow
access to the test's connection. In general, you would write this:
setup tags do
pid = Ecto.Adapters.SQL.Sandbox.start_owner!(Demo.Core.Repo, shared: not tags[:async])
on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
metadata_header = Combo.Ecto.SQL.Sandbox.metadata_for(Demo.Core.Repo, pid)
# pass the metadata to the acceptance test library
:ok
endWallaby
To write concurrent acceptance tests with Wallaby, first add it as a dependency
to your mix.exs:
{:wallaby, "~> 0.25", only: :test}Wallaby can take care of setting up the Ecto Sandbox for you if you use
use Wallaby.Feature in your test module.
defmodule Demo.Web.PageFeature do
use ExUnit.Case, async: true
use Wallaby.Feature
feature "shows some text", %{session: session} do
session
|> visit("/home")
|> assert_text("Hello world!")
end
endIf you don't use Wallaby.Feature, you can add the following to your test case
(or case template):
use Wallaby.DSL
setup tags do
pid = Ecto.Adapters.SQL.Sandbox.start_owner!(Demo.Core.Repo, shared: not tags[:async])
on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
metadata = Combo.Ecto.SQL.Sandbox.metadata_for(Demo.Core.Repo, pid)
{:ok, session} = Wallaby.start_session(metadata: metadata)
endRead more instructions for Wallaby here.
Acceptance tests with channels
To support channels, in addition the above, you need to make it so each channel is allowed within the sandbox. The first step is to access the relevant header metadata.
To do so, you must declare that you want to pass connection information to your socket. This is typically the user agent header, but it can be a custom x-header too:
socket "/path", Socket,
websocket: [connect_info: [:user_agent, …]]
socket "/path", Socket,
websocket: [connect_info: [:x_headers, …]]Now use the Combo.Socket.connect/3 callback to access the header and
store it in the socket:
# user_socket.ex
def connect(_params, socket, connect_info) do
{:ok, assign(socket, :combo_ecto_sandbox, connect_info.user_agent)}
endOr if you are using a custom header:
Enum.find_value(connect_info.x_headers, fn
{"x-my-custom-header", val} -> val
_ -> nil
end)This stores the value on the socket, so it can be available to all of your channels for allowing the sandbox.
# room_channel.ex
def join("room:lobby", _payload, socket) do
allow_ecto_sandbox(socket)
{:ok, socket}
end
# This is a great function to extract to a helper module
defp allow_ecto_sandbox(socket) do
Combo.Ecto.SQL.Sandbox.allow(
socket.assigns.combo_ecto_sandbox,
Ecto.Adapters.SQL.Sandbox
)
endallow/2 needs to be manually called once for each channel, at best directly
at the start of Combo.Channel.join/3.
Concurrent end-to-end tests with external clients
Concurrent and transactional tests for external HTTP clients is supported,
allowing for complete end-to-end tests. This is useful for cases such as
JavaScript test suites for single page applications that exercise the
Combo endpoint for end-to-end test setup and teardown. To enable this,
you can expose a sandbox route on the Combo.Ecto.SQL.Sandbox plug by
providing the :at, and :repo options. For example:
plug Combo.Ecto.SQL.Sandbox,
at: "/sandbox",
repo: Demo.Core.Repo,
timeout: 15_000 # the defaultThis would expose a route at "/sandbox" for the given repo where
external clients send POST requests to spawn a new sandbox session,
and DELETE requests to stop an active sandbox session. By default,
the external client is expected to pass up the "user-agent" header
containing serialized sandbox metadata returned from the POST request,
but this value may customized with the :header option.
Finally, make sure your repository mode is set either to :manual
or {:shared, self()} before the external client starts. This is
typically done by default in your test/test_helper.exs, but you
may need to do it explicitly depending on your setup:
Ecto.Adapters.SQL.Sandbox.mode(Demo.Core.Repo, :manual)
Summary
Functions
Decodes the given metadata and allows the current process under the given sandbox.
Decodes encoded metadata back into map generated from metadata_for/2.
Encodes metadata generated by metadata_for/2 for client response.
Returns metadata to establish a sandbox for.
Spawns a sandbox session to checkout a connection for a remote client.
Stops a sandbox session holding a connection for a remote client.
Functions
Decodes the given metadata and allows the current process under the given sandbox.
Decodes encoded metadata back into map generated from metadata_for/2.
Encodes metadata generated by metadata_for/2 for client response.
@spec metadata_for(Ecto.Repo.t() | [Ecto.Repo.t()], pid(), keyword()) :: map()
Returns metadata to establish a sandbox for.
The metadata is then passed via user-agent/headers to browsers.
Upon request, the Combo.Ecto.SQL.Sandbox plug will decode
the header and allow the request process under the sandbox.
Options
:trap_exit- if the browser being used for integration testing navigates away from a page or aborts a AJAX request while the request process is talking to the database, it will corrupt the database connection and make the test fail. Therefore, to avoid intermittent tests, we recommend trapping exits in the request process, so all database connections shut down cleanly. You can disable this behaviour by setting the option to false.
Spawns a sandbox session to checkout a connection for a remote client.
Examples
iex> {:ok, _owner_pid, metadata} = start_child(Demo.Core.Repo)
Stops a sandbox session holding a connection for a remote client.
Examples
iex> {:ok, owner_pid, metadata} = start_child(Demo.Core.Repo)
iex> :ok = stop(owner_pid)