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/3receives the id and updates:active_member.
Testing patches
I want to share two ways of testing patches:
- Using the
assert_patchhelper, 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/2test breaks because it was testing patching. - The behavior-based test continues to pass because users care that the page updates, not how it updates.