LiveView Testing: Ensuring real-time synchronization across sessions
This post is part of the Testing Series, an overview of testing strategies for Elixir applications.
Commonly overlooked in real-time applications are tests for integrations between user sessions. This post details how to effectively test actions in one session are accurately reflected in other sessions.
Test Creating a Player
Objective: Verify when user_a
creates a new player it is immediately visible to user_b
.
- Setup: Initialize two user sessions for
user_a
anduser_b
. - Action:
user_a
submits a form to create a new player with randomly generated attributes. - Verification: Confirm real-time update to
user_b
.
test "create player from user_a shows on user_b", %{conn: conn} do
attrs = %{
name: Faker.Person.name(),
email: Faker.Internet.email(),
score: Faker.random_between(0, 100_000)
}
{:ok, user_a_new_view, _html} = live(conn, @new_url)
{:ok, user_b_view, _html} = live(conn, @url)
user_a_new_view
|> form("#player-form", player: attrs)
|> render_submit()
assert_player_in_table(user_b_view, attrs)
end
Test Updating a Player
Objective: Verify when user_a
updates a player the changes are immediately visible to user_b
.
- Setup: Create a player and open sessions for
user_a
anduser_b
who both can see the player. - Action:
user_a
updates the name and score of the player. - Verification: Confirm real-time update to
user_b
.
test "update player from user_a shows on user_b", %{conn: conn, player_1: player_1} do
updated_player = %{
name: Faker.Person.name(),
email: player_1.email,
score: player_1.score + 1
}
{:ok, user_a_edit_view, _html} = live(conn, get_edit_url(player_1))
{:ok, user_b_view, _html} = live(conn, @url)
assert_player_in_table(user_b_view, player_1)
user_a_edit_view
|> form("#player-form", player: updated_player)
|> render_submit()
assert_player_in_table(user_b_view, updated_player)
end
Test Deleting a Player
Objective: Verify deleting a player by user_a
removes the player from both user_a
and user_b
views, even if user_c
attempts to update the player simultaneously.
- Setup: Add
player_1
to the database and create three user sessions,user_a
,user_b
, anduser_c
. - Action:
user_a
deletesplayer_1
. - Interference:
user_c
editsplayer_1
after the deletion. - Verification: Confirm real-time update of
user_a
anduser_b
.
test "delete player_1 from user_a removes player from both user_a and user_b", %{
conn: conn,
player_1: player_1
} do
updated_player = %{
name: Faker.Person.name(),
email: player_1.email,
score: player_1.score + 1
}
{:ok, user_a_view, _html} = live(conn, @url)
{:ok, user_b_view, _html} = live(conn, @url)
{:ok, user_c_view, _html} = live(conn, get_edit_url(player_1))
player_row = "#players-#{player_1.id}"
assert has_element?(user_a_view, player_row)
assert has_element?(user_b_view, player_row)
user_a_view
|> render_hook("delete", %{"id" => player_1.id})
# Another user editing a player should not reverse the deletion
user_c_view
|> form("#player-form", player: updated_player)
|> render_submit()
refute has_element?(user_a_view, player_row)
refute has_element?(user_b_view, player_row)
end
end