Testing a Counter
We’ll test a canonical example of interaction: a counter.
In our router, you should see that we have a CounterLive mounted under /counter.
# lib/ranger_web/router.ex
live "/counter", CounterLive
CounterLive
defmodule RangerWeb.CounterLive do
use RangerWeb, :live_view
def render(assigns) do
~H"""
<div class="max-w-xs mx-auto flex justify-center space-x-10 border-2 p-8">
<.button id="decrement" phx-click="decrease" class="bg-red-500 hover:bg-red-700" type="button">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15" />
</svg>
</.button>
<div id="count" class="font-extrabold text-3xl"><%= @count %></div>
<.button id="increment" phx-click="increase" class="bg-blue-500 hover:bg-blue-700" type="button">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
</.button>
</div>
"""
end
def mount(_, _, socket) do
{:ok, assign(socket, :count, 0)}
end
def handle_event("increase", _, socket) do
{:noreply, update(socket, :count, fn count -> count + 1 end)}
end
def handle_event("decrease", _, socket) do
{:noreply, update(socket, :count, fn count -> count - 1 end)}
end
end
Our CounterLive holds a simple counter in its state. We can increment it or decrement it. And the LiveView renders buttons to decrement and increment it as well as showing the current counter.
Initial test
Let’s go ahead and open the test for it in test/ranger_web/live/counter_live_test.exs:
defmodule RangerWeb.CounterLiveTest do
use RangerWeb.ConnCase
import Phoenix.LiveViewTest
end
Let’s add our first test, user can increase counter.
The exercise is simple, but it’ll help us get acquainted with LiveViewTest’s tools. We’ll use our traditional setup with live/2, and we’ll use the view since we want to interact with the page before making assertions:
test "user can increase counter", %{conn: conn} do
{:ok, view, _} = live(conn, ~p"/counter")
end
Then, we want to use two LiveView helpers to target an element on the page and click on it:
Phoenix.LiveViewTest.element/3takes three arguments: a view, a CSS selector, and an optional text filter (to scope whatever action we take to that element).render_click/3is what I call an action function. It performs an action on the view or on a scoped element of the view. There are many such functions likerender_blur,render_keydown,render_submit, etc. We’ll look into some of those in future videos. Allrender_*functions return the HTML string that is rendered after the action is done.
Let’s put those into practice.
Grab the view, pass it to the element/3 helper, and let’s target an element with a #increment ID. Then let’s click on it.
# test/ranger_web/live/counter_live_test.exs
test "user can increase counter", %{conn: conn} do
{:ok, view, _} = live(conn, ~p"/counter")
view
|> element("#increment")
|> render_click()
end
Once we’ve done that, let’s assert that the HTML that is rendered by that has a count of "1":
test "user can increase counter", %{conn: conn} do
{:ok, view, _} = live(conn, ~p"/counter")
view
|> element("#increment")
|> render_click()
assert has_element?(view, "#count", "1")
end
Let’s run the test:
mix test test/ranger_web/live/counter_live_test.exs
.
Finished in 0.1 second
1 test, 0 failures, 0 excluded
Excellent.
But before we move on, let’s talk a little bit more about two alternative ways of writing this test (and their downsides): changing the assertion and changing the action function.
Alternative assertion
Each render function returns an HTML blob. So, we could have technically written our test like this:
test "user can increase counter", %{conn: conn} do
{:ok, view, _} = live(conn, ~p"/counter")
html =
view
|> element("#increment")
|> render_click()
assert html =~ "1"
end
If we run that test, it passes. So why not do that? It seems simpler. As we’ve discussed in a previous lesson, using the has_element?/3 helper allows us to have more precise assertions.
When we write assert html =~ "1" we’re simply asserting there’s a "1" anywhere in our HTML blob. Do you have an <h1> tag? That test will pass incorrectly.
You can see it in iex:
iex(1)> "<h1>" =~ "1"
true
Let’s confirm that by removing the @count from our CounterLive and adding an h1 heading:
+ <h1>Counter</h1>
<div class="max-w-xs mx-auto flex justify-center space-x-10 border-2 p-8">
<.button id="decrement" phx-click="decrease" class="bg-red-500 hover:bg-red-700" type="button">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15" />
</svg>
</.button>
-
- <div id="count" class="font-extrabold text-3xl"><%= @count %></div>
<.button id="increment" phx-click="increase" class="bg-blue-500 hover:bg-blue-700" type="button">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
</.button>
</div>
If you run the test now, it passes because it finds the "1" in h1.
That is why our has_element?/3 helper is so valuable.
Alternative action function
Before we finish this lesson, I want to talk about an alternative version of the render_click function.
If you look at the docs, render_click has two varieties: render_click/3 and render_click/2.
Thus far, we’ve been using render_click/2 where we pass an element to it. But we could also use render_click/3, which takes in the view, the event, and an optional value directly.
Let’s write the test like that:
test "user can increase counter", %{conn: conn} do
{:ok, view, _} = live(conn, ~p"/counter")
render_click(view, "increase")
assert has_element?(view, "#count", "1")
end
As you can see, we can render the "increase" event. (Note that it’s different from our #increment ID.)
Instead of using element/3 to find an element with #increment and clicking on it, we can instead call render_click/3 directly passing the event and the value.
In this case, we don’t have a value to pass, so we simply call render_click(view, "increase").
Seems simpler, right?
So you may rightly wonder, why not use this test?
The problem is that this test is bypassing our HTML and sending the event directly to our CounterLive.
Let’s confirm that by removing the + button from our HTML.
# lib/ranger_web/live/counter_live.ex
<div class="max-w-xs mx-auto flex justify-center space-x-10 border-2 p-8">
<.button id="decrement" phx-click="decrease" class="bg-red-500 hover:bg-red-700" type="button">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15" />
</svg>
</.button>
<div id="count" class="font-extrabold text-3xl"><%= @count %></div>
-
- <.button id="increment" phx-click="increase" class="bg-blue-500 hover:bg-blue-700" type="button">
- <svg
- xmlns="http://www.w3.org/2000/svg"
- fill="none"
- viewBox="0 0 24 24"
- stroke-width="1.5"
- stroke="currentColor"
- class="w-6 h-6"
- >
- <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
- </svg>
- </.button>
Now rerun the test.
mix test test/ranger_web/live/counter_live_test.exs
.
Finished in 0.1 seconds
1 test, 0 failures, 0 excluded
It still passes.
The test is misleading us into thinking that a user can increase the count. But our production code will be broken.
To compare, let’s run our test that uses element/3:
mix test test/ranger_web/live/counter_live_done_test.exs:10
Compiling 1 file (.ex)
Excluding tags: [:test]
Including tags: [line: "10"]
1) test user can increase counter (RangerWeb.CounterLiveDoneTest)
test/ranger_web/live/counter_live_done_test.exs:6
** (ArgumentError) expected selector "#increment" to return a single element, but got none
Finished in 0.2 seconds (0.00s async, 0.2s sync)
4 tests, 1 failure, 3 excluded
It fails because it cannot find an element with "#increment" ID, just like we would expect if we didn’t have the button there.
So, my advice is to use the element/3 helper in combination with render actions unless you have a good reason to try to bypass the HTML markup.
Bonus exercise
If you’re up for it, go ahead and add a test for the - button.
You can see how element/3 helps you by removing different portions of the HTML.
For example, what happens if you remove the entire <button>? What about if you only remove the phx-click attribute?
Resources
element/3helper: https://hexdocs.pm/phoenix_live_view/Phoenix.LiveViewTest.html#element/3render_click/2helper: https://hexdocs.pm/phoenix_live_view/Phoenix.LiveViewTest.html#render_click/2render_click/3helper: https://hexdocs.pm/phoenix_live_view/Phoenix.LiveViewTest.html#render_click/3has_element?/3helper: https://hexdocs.pm/phoenix_live_view/Phoenix.LiveViewTest.html#has_element?/3