View Source Phoenix.Ecto.SQL.Sandbox (Phoenix/Ecto v4.5.1)

A plug to allow concurrent, transactional acceptance tests with Ecto.Adapters.SQL.Sandbox.

Example

This plug should only be used during tests. First, set a flag to enable it in config/test.exs:

config :your_app, sql_sandbox: true

And use the flag to conditionally add the plug to lib/your_app/endpoint.ex:

if Application.compile_env(:your_app, :sql_sandbox) do
  plug Phoenix.Ecto.SQL.Sandbox
end

It's important that this is at the top of endpoint.ex, before any other plugs.

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 lines, you would write this:

setup tags do
  pid = Ecto.Adapters.SQL.Sandbox.start_owner!(YourApp.Repo, shared: not tags[:async])
  on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
  metadata_header = Phoenix.Ecto.SQL.Sandbox.metadata_for(YourApp.Repo, pid)
  # pass the metadata to the acceptance test library
  :ok
end

You can follow the 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 c:Phoenix.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, :phoenix_ecto_sandbox, connect_info.user_agent)}
end

Or 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
  Phoenix.Ecto.SQL.Sandbox.allow(
    socket.assigns.phoenix_ecto_sandbox,
    Ecto.Adapters.SQL.Sandbox
  )
end

allow/2 needs to be manually called once for each channel, at best directly at the start of c:Phoenix.Channel.join/3.

Acceptance tests with LiveViews

LiveViews can be supported in a similar fashion to channels. First declare the :user_agent (or a custom header) in your live socket configuration in endpoint.ex:

socket "/live", Phoenix.LiveView.Socket,
  websocket: [connect_info: [:user_agent, session: @session_options]]

Now you can use the on_mount/4 callback to check the header and assign the sandbox:

defmodule MyApp.LiveAcceptance do
  def on_mount(:default, _params, _session, socket) do
    %{assigns: %{phoenix_ecto_sandbox: metadata}} =
      assign_new(socket, :phoenix_ecto_sandbox, fn ->
        if connected?(socket), do: get_connect_info(socket, :user_agent)
      end)

    Phoenix.Ecto.SQL.Sandbox.allow(metadata, Ecto.Adapters.SQL.Sandbox)
  end
end

Now, in your my_app_web.ex file, you can invoke this callback for all of your LiveViews if the sandbox configuration, defined at the beginning of the documentation, is enabled:

def live_view do
  quote do
    use Phoenix.LiveView
    # ...

    if Application.compile_env(:your_app, :sql_sandbox) do
      on_mount MyApp.LiveAcceptance
    end

    # ...
  end
end

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 Phoenix endpoint for end-to-end test setup and teardown. To enable this, you can expose a sandbox route on the Phoenix.Ecto.SQL.Sandbox plug by providing the :at, and :repo options. For example:

plug Phoenix.Ecto.SQL.Sandbox,
  at: "/sandbox",
  repo: MyApp.Repo,
  timeout: 15_000 # the default

This 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(MyApp.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

Link to this function

allow(encoded_metadata, sandbox)

View Source

Decodes the given metadata and allows the current process under the given sandbox.

Link to this function

decode_metadata(encoded_meta)

View Source

Decodes encoded metadata back into map generated from metadata_for/2.

Link to this function

encode_metadata(metadata)

View Source

Encodes metadata generated by metadata_for/2 for client response.

Link to this function

metadata_for(repo_or_repos, pid, opts \\ [])

View Source
@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 Phoenix.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.
Link to this function

start_child(repos, opts \\ [])

View Source

Spawns a sandbox session to checkout a connection for a remote client.

Examples

iex> {:ok, _owner_pid, metadata} = start_child(MyApp.Repo)

Stops a sandbox session holding a connection for a remote client.

Examples

iex> {:ok, owner_pid, metadata} = start_child(MyApp.Repo)
iex> :ok = stop(owner_pid)