I’m running into an edge case with phoenix testing. I’ve got an on_mount handler behaving like a plug which creates a db session for a given client:
defmodule SgWeb.InitSessionId do
@moduledoc """
Adds the DB session ID to the assigns
"""
import Phoenix.Component
alias Sg.Sessions
def on_mount(:default, _params, %{"client_id" => client_id}, socket) do
session =
case Sessions.get_session_by_client_id(client_id) do
nil ->
{:ok, session} = Sessions.create_session(%{client_id: client_id})
session
existing_session ->
existing_session
end
{:cont, assign(socket, session_id: session.id)}
end
end
However, whenever I try to test this as one would a plug, the assign function blows up and tells me I need to use render_component. I understand the logic, generally, but there has to be a way around this - I’m literally following a use case described in the docs in this instance (Phoenix.LiveView — Phoenix LiveView v0.19.5). Is there an easy way to mock a socket/assigns without triggering the exceptions?
Maybe you’re confusing the render
functions from Phoenix.{LiveView, LiveComponent, Component}
with the render_component
function from Phoenix.LiveViewTest
?
render_component/3
is meant to be used in tests to simulate rendering a component and accepts arbitrary assigns.
import Phoenix.LiveViewTest
test "greets" do
assert render_component(&MyComponents.greet/1, name: "Mary") ==
"<div>Hello, Mary!</div>"
end
source: render_component/3 | Phoenix.LiveViewTest docs
No I understand, but this is an on_mount callback, not a LV/LC etc. I just want to be able to unit test it.
** (ArgumentError) assign/3 expects a socket from Phoenix.LiveView/Phoenix.LiveComponent or an assigns map from Phoenix.Component as first argument, got: %{}You passed an assigns map that does not have the relevant change tracking information. This typically means you are calling a function component by hand instead of using the HEEx template syntax.
my use of assign/2
is causing the above error in my test.
Can you show the test code you are trying to make work?
Sure, super simple
defmodule SgWeb.InitSessionIdTest do
use Sg.DataCase
alias Sg.Sessions
test "creates a session db record if one not present" do
session = %{"client_id" => "test123"}
SgWeb.InitSessionId.on_mount(:default, nil, session, %{})
assert Sessions.get_session_by_client_id("test123").client_id == "test123"
end
end
the empty map where the socket should be is just a plaeholder of one of many and varied attempts to mock a socket
I’m very happy to leave this untested, as it’s pretty uncontroversial what it does, but I’m in it now and want to solve this for future reference as no doubt going to need to set assigns in live_session hooks again.
defmodule SgWeb.InitSessionIdTest do
use Sg.DataCase
alias Sg.Sessions
import Phoenix.ChannelTest
@endpoint SgWeb.Endpoint
test "creates a session db record if one not present" do
socket = socket(Phoenix.LiveView.Socket)
session = %{"client_id" => "test123"}
SgWeb.InitSessionId.on_mount(:default, nil, session, socket)
assert Sessions.get_session_by_client_id("test123").client_id == "test123"
end
end
A little bit closer, but assigns still complains that it’s not a LV socket…
** (ArgumentError) assign/3 expects a socket from Phoenix.LiveView/Phoenix.LiveComponent or an assigns map from Phoenix.Component as first argument, got: %Phoenix.Socket{assigns: %{}, channel: nil, channel_pid: nil, endpoint: SgWeb.Endpoint, handler: Phoenix.LiveView.Socket, id: nil, joined: false, join_ref: nil, private: %{}, pubsub_server: Sg.PubSub, ref: nil, serializer: Phoenix.ChannelTest.NoopSerializer, topic: nil, transport: {Phoenix.ChannelTest, #PID<0.507.0>}, transport_pid: #PID<0.504.0>}
code: SgWeb.InitSessionId.on_mount(:default, nil, session, socket)
stacktrace:
(phoenix_live_view 0.19.5) lib/phoenix_component.ex:1198: Phoenix.Component.raise_bad_socket_or_assign!/2
(elixir 1.15.2) lib/enum.ex:2510: Enum."-reduce/3-lists^foldl/2-0-"/3
(sg 0.1.0) lib/sg_web/live/init_session_id.ex:19: SgWeb.InitSessionId.on_mount/4
test/sg_web/live/init_session_id_test.exs:11: (test)
Got it! Used this as a ‘socket’ socket = %{session_id: 2, __changed__: %{client_id: true}}
I feel like unit testing these with a synthetic socket isn’t likely to maintain well over time. Maybe Phoenix offers a way to instantiate a socket?
Have you tried this?
socket = %Phoenix.LiveView.Socket{}
It seemed to do the trick for me.
Instead you should integration test this using the live/2
primitives. You can test both good and bad auth (redirects), and do the same assert Sessions.get_session_by_client_id("test123").client_id == "test123"
at the end.
I swore I tried that before…anyway yes that actually worked. I think I was trying to build a socket but seems a raw struct will also do.
I should be clear that I was just testing out your example and think Chris’ suggestion is the way to go.
Personally, I forgo unit testing these hooks and just test their effects in each LiveView they’re used in. I make a helper for this, kind of like how phx_gen_auth
does with its generated register_and_log_in_user
test helper. But obviously if you want a unit test that’s absolutely fine, I’m just sharing!
Speaking of phx_gen_auth
, it actually does generate unit tests for its hooks that look like your tests! For example:
test "authenticates current_user based on a valid user_token ", %{conn: conn, user: user} do
user_token = Accounts.generate_user_session_token(user)
session = conn |> put_session(:user_token, user_token) |> get_session()
{:cont, updated_socket} =
UserAuth.on_mount(:ensure_authenticated, %{}, session, %LiveView.Socket{})
assert updated_socket.assigns.current_user.id == user.id
end
1 Like
Thanks for responding. I think I much prefer the unit style for middleware personally.