I wanted to run some LiveTests with
use MyApp.ConnCase, async: true
but I was having a problem because, in async mode, each process gets its own Sandbox connection.
In a default Phoenix app, this is not a problem because of this line in test_helper.exs
:
Ecto.Adapters.SQL.Sandbox.mode(MyApp.Repo, :manual)
However, I removed that line from my project because in manual mode tests break when using worker processes in application.ex that access the database.
This means that data available to the 1st mount is different to the data available in the 2nd mount.
I humbly present one “hacky” way of making LiveView tests async.
We want to “allow” the 2nd mount process to use the 1st mount processes DB connection, so:
Make an on_mount
module
defmodule MyAppWeb.OnMounts.AllowSandboxForLiveView do
@moduledoc false
alias Ecto.Adapters.SQL.Sandbox
alias MyApp.Repo
alias Phoenix.LiveView
def on_mount(:default, _params, session, socket) do
socket
|> tap(&allow_sandbox(&1, session))
|> then(&{:cont, &1})
end
defp allow_sandbox(socket, session) do
with true <- LiveView.connected?(socket),
unconnected_pid when is_pid(unconnected_pid) <- Map.get(session, "unconnected_pid"),
true <- self() != unconnected_pid do
Sandbox.allow(Repo, unconnected_pid, self())
end
end
end
Make a plug to get the PID in the session
defmodule MyAppWeb.Plugs.PutUnconnectedPidInSession do
@moduledoc """
Put the PID in the session, for testing purposes ONLY.
"""
import Plug.Conn
def init(_opts), do: nil
if Application.compile_env(:my_app, :env) == :test do
def call(conn, _opts), do: put_session(conn, :unconnected_pid, self())
else
def call(conn, _opts), do: conn
end
end
Use the plug in router.ex
pipeline :browser do
# ...
if Application.compile_env(:my_app, :env) == :test do
plug MyAppWeb.Plugs.PutUnconnectedPidInSession
end
#...
Use the on_mount
in all LiveViews. Go to my_app_web.ex
def live_view do
#...
if Application.compile_env(:my_app, :env) == :test do
on_mount MyAppWeb.OnMounts.AllowSandboxForLiveView
end
#...
And, finally, use the on_mount
in all live_session
in the router.ex
.
if Application.compile_env(:my_app, :env) == :test do
@preliminary_on_mounts [MyAppWeb.OnMounts.AllowSandboxForLiveView]
else
@preliminary_on_mounts []
end
live_session,
#...
on_mount: @preliminary_on_mounts ++ [
AssignUser,
RequireUser,
#...etc
This is hacky and a bit ugly in the live_session
bit but it makes LiveView work with
use MyApp.ConnCase, async: true
What is happening here:
As far as I know, the only way to share data between disconnected and connected mount is to use the session. In test environment only, put the PID in the session. Then during the connected mount use that PID to allow access to the Sandbox connection.
Note: if you want the Application.compile_env(:my_app, :env)
to work then just add this line to config/test.exs
:
config :my_app, :env, :test
And similar for dev/prod.
I eagerly welcome any and all feedback.