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.

See the code

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 and user_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 and user_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, and user_c.
  • Action: user_a deletes player_1.
  • Interference: user_c edits player_1 after the deletion.
  • Verification: Confirm real-time update of user_a and user_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