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_aanduser_b. - Action:
user_asubmits 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_aanduser_bwho both can see the player. - Action:
user_aupdates 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_1to the database and create three user sessions,user_a,user_b, anduser_c. - Action:
user_adeletesplayer_1. - Interference:
user_ceditsplayer_1after the deletion. - Verification: Confirm real-time update of
user_aanduser_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