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. 🎉