Using has_element?/3 for better LiveViewTest assertions

When writing LiveView tests, I often see people use a lot of HTML markup in their assertions. It’s a logical way to write assertions since LiveViewTest’s render functions return an HTML string.

For example, we might have the following test:

test "user can see avatar in profile", %{conn: conn} do
  user = insert(:user)
  {:ok, view, _html} = live(conn, "/")

  html =
    view
    |> element("#show-profile")
    |> render_click()

  assert html =~ ~s(<img src="#{user.avatar_url}")
end

Here, we’re tempted to include the <img portion in our assertion to ensure the avatar’s URL is part of an img tag, not just a string somewhere else in the HTML. But that creates unnecessary coupling between our test and the implementation specifics.

Suppose a teammate comes along and decides to add a CSS class to that img tag:

- <img src="<%= @current_user.avatar_url %>">
+ <img class="user-avatar" src="<%= @current_user.avatar_url %>">

Suddenly, our test breaks even though we haven’t changed the behavior of our LiveView at all. 😞

Thankfully, there’s a better way.

Using the has_element?/3 helper

LiveViewTest comes with a has_element?/3 helper that allows us to be specific in our assertions (using CSS selectors) without adding coupling.

The has_element?/3 helper takes in a view, a CSS selector, and an optional text filter (which we don’t need in this case). So, we can rewrite our test like this:

test "user can see avatar in profile", %{conn: conn} do
  user = insert(:user)
  {:ok, view, _html} = live(conn, "/")

  view
  |> element("#show-profile")
  |> render_click()

  assert has_element?(view, "img[src*=#{user.avatar_url}]")
end

Our test is now specific enough to ensure the avatar’s URL is part of the img tag, but it remains decoupled from the implementation details. Our teammates can add all the CSS classes and data attributes they want. The test will continue to pass. 🎉

Want my latest thoughts, posts, and projects in your inbox?

    I will never send you spam. Unsubscribe any time.