Browse lessons

Testing Live Patch

In the last lesson, we saw the first half of the live navigation equation. In this lesson, we want to see how we should test live patching.

DirectoryLive

If you open your router, you’ll see we have a couple of DirectoryLive routes mounted under /directory paths:

live "/directory", DirectoryLive, :index
live "/directory/:id", DirectoryLive, :show

If you launch your server and visit /directory, you’ll see that when we select someone, we’re performing live patches.

Here’s the code for DirectoryLive:

defmodule RangerWeb.DirectoryLive do
  use RangerWeb, :live_view

  alias Ranger.Directory

  def render(assigns) do
    ~H"""
    <div class="flex h-full">
      <div class="flex min-w-0 flex-1 flex-col overflow-hidden">
        <div class="flex flex-1 overflow-hidden">
          <aside class="w-96 flex-shrink-0 border-r border-gray-200 flex flex-col">
            <div class="px-6 pt-6 pb-4">
              <h2 class="text-lg font-medium text-gray-900">Directory</h2>
            </div>
            <nav class="min-h-0 flex-1 overflow-y-auto" aria-label="Directory">
              <div class="relative">
                <ul role="list" class="relative divide-y divide-gray-200">
                  <%= for member <- @members do %>
                    <li>
                      <div class="relative flex items-center space-x-3 px-6 py-5 hover:bg-gray-50">
                        <div class="flex-shrink-0">
                          <img class="h-10 w-10 rounded-full" src={member.image_url} alt="" />
                        </div>
                        <div class="min-w-0 flex-1">
                          <.link
                            data-role="member"
                            patch={~p"/directory/#{member.id}"}
                            class="focus:outline-none"
                          >
                            <span class="absolute inset-0" aria-hidden="true"></span>
                            <p class="text-sm font-medium text-gray-900"><%= member.name %></p>
                            <p class="truncate text-sm text-gray-500"><%= member.type %></p>
                          </.link>
                        </div>
                      </div>
                    </li>
                  <% end %>
                </ul>
              </div>
            </nav>
          </aside>

          <main class="relative flex-1 overflow-y-auto focus:outline-none">
            <article id="active-member">
              <h1 class="truncate text-2xl font-bold text-gray-900"><%= @active_member.name %></h1>
            </article>
          </main>
        </div>
      </div>
    </div>
    """
  end

  def mount(_, _, socket) do
    members = Directory.all_members()
    active_member = hd(members)

    {:ok, assign(socket, members: members, active_member: active_member)}
  end

  def handle_params(%{"id" => id}, _, socket) do
    member = Directory.get_by(id: String.to_integer(id))

    {:noreply, assign(socket, :active_member, member)}
  end

  def handle_params(_, _, socket) do
    {:noreply, socket}
  end
end

There’s a lot of text there, but the functionality is simple:

  • When we mount the LiveView, we fetch a list of members and set the first one as :active_member.
  • In render, we iterate through members and render links with patch={~p"/directory/#{member.id}"}.
  • Clicking a link patches the current LiveView.
  • handle_params/3 receives the id and updates :active_member.

Testing patches

I want to share two ways of testing patches:

  • Using the assert_patch helper, and
  • Focusing on behavior and not implementation.

Testing with assert_patch/2

defmodule RangerWeb.DirectoryLiveDoneTest do
  use RangerWeb.ConnCase, async: true

  import Phoenix.LiveViewTest

  alias Ranger.Directory

  test "user is directed to member information (assert_patch)", %{conn: conn} do
    member = Directory.get_by(name: "Aragorn")
    {:ok, view, _html} = live(conn, ~p"/directory")

    view
    |> element("[data-role=member]", "Aragorn")
    |> render_click()

    assert_patch(view, ~p"/directory/#{member.id}")
  end
end

The test works, and it’s what assert_patch/2 was created for.

But it leaves something to be desired if we think about testing from the perspective of the user.

In particular, why does user care that the URL is there? Or that we patched?

We’re testing an implementation detail. Let’s see how we can test the behavior instead.

Testing behavior

test "user can see member information upon selection", %{conn: conn} do
  {:ok, view, _html} = live(conn, ~p"/directory")

  view
  |> element("[data-role=member]", "Aragorn")
  |> render_click()

  assert has_element?(view, "#active-member", "Aragorn")
end

This test focuses on behavior as seen through our users’ eyes.

It doesn’t care if we’re using live_patch or something else, and for that reason, it’s a more robust test.

URLs as first-class citizens

Of course, it’s possible our users actually care about the URL. Maybe it’s something we want them to share.

In that case, we could write a test that captures that requirement through our users’ eyes:

test "user can visit specific member", %{conn: conn} do
  member = Directory.get_by(name: "Aragorn")

  {:ok, view, _html} = live(conn, ~p"/directory/#{member.id}")

  assert has_element?(view, "#active-member", "Aragorn")
end

That test has the benefit of documenting that the URL is important for our application, while still testing behavior.

Combine assert_patch with behavior test

test "combine assert_patch and behavior testing", %{conn: conn} do
  member = Directory.get_by(name: "Aragorn")
  {:ok, view, _html} = live(conn, ~p"/directory")

  view
  |> element("[data-role=member]", "Aragorn")
  |> render_click()

  assert_patch(view, ~p"/directory/#{member.id}")
  assert has_element?(view, "#active-member", "Aragorn")
end

This is another option, but it suffers from the same issue as the original assert_patch/2-only test: it’s not robust to implementation changes.

Example: change implementation to phx-click

To drive the point home, let’s take a look at how assert_patch/2 ties us to implementation.

If we change <.link patch={}> to a <div> with phx-click="set-active-member" and phx-value-id={member.id}, and move logic to handle_event("set-active-member", ...):

  • The assert_patch/2 test breaks because it was testing patching.
  • The behavior-based test continues to pass because users care that the page updates, not how it updates.