Home Posts Post Search Tag Search

LiveView 27 - Chapter 10: Test Your Live Views
Published on: 2026-02-12 Tags: elixir, Blog, Testing, LiveView, Html/CSS, Phoenix, Framework

Chapter 10: Test Your Live Views (311)

Testing!!!! This is where all the work we have done really comes to something. We now need to test the functionality of our code. Does it really do what we want? Can it handle some stress testing? With the framework that we have set we can start to build robust tests.


What Makes CRC Code Testable? (312)

For any test we are trying to verify that a certain pre-condition will lead to and expected outcome. Each test should do 3 thing:


Set a precondition


Provide a stimulus


Compare an actual response to expectations


We will be testing by integrating into the LiveView machinery to examine the impact of page loads and events.


There are a few different test that we can run for our site:


integration tests - this will test the machinery of the site. Things between the client and the server.


unit tests - this will test specific functions to see if they return the right values or data sets.


Isolation vs Integration

This boils down to the amount of things that you want to test. Unit tests can really make a testers job easier. You build a new function and instead of running to an iex session you can build a test and then send the same inputs and get the desired outcome if done correctly. You can build a lot of test really fast.


Integration is when we want to test more than one thing at a time or need to deal with more than one function to get to the result that we want.


In this section you will build both unit and integration tests. Pure unit test will use ExUnit. While integration test will use the LiveViewTest features. We will even use the plain Elixir PubSub to pass messages.

Start with Units Tests

This is one of the most important parts when working with integration, as you don’t want to test functionality of a CRC when we are in the integration phase. We want robust unit tests so that when we get to integration we are focused on the page.

Unit Test Test Survey Results State (315)

First lets deal with how the reducer for assign_products_with_average_ratings/1 works. If you don’t have this module within your test folder create it now. pento/test/pento_web/live/survey_results_live.exs

defmodule PentoWeb.SurveyResultsLiveTest do
  use PentoWeb.ConnCase
  alias PentoWeb.Admin.SurveyResultsLive
  # This is the normal way to do this but we will work on creating all the functions
  # here so that we can see how it all comes together.
  import Pento.{SurveyFixtures, CatalogFixtures, AccountFixtures}
end

Now we can populate the builders (fixtures) for creating users, creating products, creating demos, creating ratings.

alias Pento.{Accounts, Survey, Catalog}

  @create_product_attrs %{
    description: "test description",
    name: "Test Game",
    sku: 42,
    unit_price: 120.5
  }
  @create_user_attrs %{
    email: "test@test.com",
    password: "passwordpassword"
  }
  @create_user2_attrs %{
    email: "another-person@email.com",
    password: "passwordpassword"
  }
  @create_demographic_attrs %{
    gender: "female",
    year_of_birth: DateTime.utc_now().year - 15,
    education: "high school"
  }
  @create_demographic2_attrs %{
    gender: "male",
    year_of_birth: DateTime.utc_now().year - 30,
    education: "college"
  }

  defp product_fixture(scope) do
    {:ok, product} = Catalog.create_product(scope, @create_product_attrs)
    product
  end

  defp user_fixture(attrs \\ @create_user_attrs) do
    {:ok, user} = Accounts.register_user(attrs)
    user
  end

  defp demographic_fixture(scope, user, attrs \\ @create_demographic_attrs) do
    attrs =
      attrs
      |> Map.merge(%{user_id: user.id})

    {:ok, demographic} = Survey.create_demographic(scope, attrs)
    demographic
  end

  defp rating_fixture(scope, stars, user, product) do
    {:ok, rating} =
      Survey.create_rating(scope, %{
        stars: stars,
        user_id: user.id,
        product_id: product.id
      })

    rating
  end

  defp create_product(%{scope: scope}) do
    product = product_fixture(scope)
    %{product: product}
  end

  defp create_user(_) do
    user = user_fixture()
    %{user: user}
  end

  defp create_rating(scope, stars, user, product) do
    rating = rating_fixture(scope, stars, user, product)
    %{rating: rating}
  end

  defp create_demographic(scope, user) do
    demographic = demographic_fixture(scope, user)
    %{demographic: demographic}
  end

  defp create_socket(_) do
    %{socket: %Phoenix.LiveView.Socket{}}
  end

Update Existing Fixtures for Phoenix 1.8

Within this part we talk about how we need to update the fixtures to reflect the new scope for 1.8, my code already has this within it but I’ll put the code here so that you can see what it should look like if your code is not up to date. pento/test/support/fixtures/survey_fixtures.ex

import Pento.CatalogFixtures

  @doc """
  Generate a demographic.
  """
  def demographic_fixture(scope, attrs \\ %{}) do
    attrs =
      Enum.into(attrs, %{
        gender: "Male",
        year_of_birth: 1990,
        education: "high school"
      })

    {:ok, demographic} = Pento.Survey.create_demographic(scope, attrs)
    demographic
  end

  @doc """
  Generate a rating.
  """
  def rating_fixture(scope, attrs \\ %{}) do
    product = product_fixture(scope)

    attrs =
      Enum.into(attrs, %{
        stars: 4,
        product_id: product.id
      })

    {:ok, rating} = Pento.Survey.create_rating(scope, attrs)
    rating
  end

Okay so let’s create our first test. Let’s start with a describe block and some setup.

  describe "Socket state" do
    setup [
      :create_user,
      :create_socket,
      :register_and_log_in_user,
      :create_product
    ]

    setup %{user: user, scope: scope} do
      create_demographic(scope, user)
      user2 = user_fixture(@create_user2_attrs)
      scope2 = Accounts.Scope.for_user(user2)
      demographic_fixture(scope2, user2, @create_demographic2_attrs)
      [user2: user2, scope2: scope2]
    end

    test "no ratings exist", %{socket: socket} do
    end
  end

Lets talk about what we just did. We have 2 setup reducers that will give us the stating state of the tests.

  # This runs the functions that we create below named as atoms.
  # This will create a user, create a socket for that user,
  # register the user, and then create a product for the user.
  setup [
      :create_user,
      :create_socket,
      :register_and_log_in_user,
      :create_product
  ]

  # This is the next part of the setup as it will take the user
  # and then create a demo for that user, create a second user,
  # get the scope for that user and then create a demo for that 
  # second user. Then both users will get passed as a map for the 
  # the next step.
  setup %{user: user, scope: scope} do
      create_demographic(scope, user)
      user2 = user_fixture(@create_user2_attrs)
      scope2 = Accounts.Scope.for_user(user2)
      demographic_fixture(scope2, user2, @create_demographic2_attrs)
      [user2: user2, scope2: scope2]
  end

Okay so we now want to create a test that will show us that we have 0 ratings for a product. When we use the assign_products_with_average_ratings/1 we expect to see the ratings for a product and if we have no ratings for a product we should see.

[{"Test Game", 0}]

So in that vein we want to do the following. Keep in mind that I had to add in some of the filters to the assigns to get this to work.

    test "no ratings exist", %{socket: socket} do
      socket =
        socket
        |> SurveyResultsLive.assign_gender_filter()
        |> SurveyResultsLive.assign_age_group_filter()
        |> SurveyResultsLive.assign_products_with_average_ratings()

      assert socket.assigns.products_with_average_ratings == [{"Test Game", 0}]
    end

Integration Test LiveView Interactions (324)

This is where we will add in the test to the way that the site works. Clicks and other types of functionality. We will need to have a socket for this one as well. We will use the LiveViewTest module for this as well. We will mount and render and then trigger events.


Write and Integration Test

We want to simulate a visit to the route /admin/dashboard, followed by some sort of filter adjustments.


Set Up The LiveView Test Module

First lets create the new file for the integration test. test/pento_web/live/admin_dashboard_live_test.exs

defmodule PentoWeb.AdminDashboardLiveTest do
  use PentoWeb.ConnCase
  import Phoenix.LiveViewTest
  alias Pento.{Accounts, Survey, Catalog}

  @create_product_attrs %{
    description: "test description",
    name: "Test Game",
    sku: 42,
    unit_price: 120.5
  }
  @create_demographic_attrs %{
    gender: "female",
    year_of_birth: DateTime.utc_now().year - 15
  }
  @create_demographic_over_18_attrs %{
    gender: "female",
    year_of_birth: DateTime.utc_now().year - 30
  }
  @create_user_attrs %{email: "test@test.com", password: "passwordpassword"}
  @create_user2_attrs %{email: "test2@test.com", password: "passwordpassword"}
  @create_user3_attrs %{email: "test3@test.com", password: "passwordpassword"}
  defp product_fixture(scope) do
    {:ok, product} = Catalog.create_product(scope, @create_product_attrs)
    product
  end

  defp user_fixture(attrs \\ @create_user_attrs) do
    {:ok, user} = Accounts.register_user(attrs)
    user
  end

  defp demographic_fixture(scope, user, attrs) do
    attrs =
      attrs
      |> Map.merge(%{user_id: user.id})

    {:ok, demographic} = Survey.create_demographic(scope, attrs)
    demographic
  end

  defp rating_fixture(scope, user, product, stars) do
    {:ok, rating} =
      Survey.create_rating(scope, %{
        stars: stars,
        user_id: user.id,
        product_id: product.id
      })

    rating
  end

  defp create_product(%{scope: scope}) do
    product = product_fixture(scope)
    %{product: product}
  end

  defp create_user(_) do
    user = user_fixture()
    %{user: user}
  end

  defp create_demographic(scope, user, attrs \\ @create_demographic_attrs) do
    demographic = demographic_fixture(scope, user, attrs)
    %{demographic: demographic}
  end

  defp create_rating(scope, user, product, stars) do
    rating = rating_fixture(scope, user, product, stars)
    %{rating: rating}
  end
end

The book has us do all of this by creating it’s own functions to create all the products, ratings, demos, users. There is a built-in function that can :register_and_log_in_user that the book uses but we can also leverage the fixtures that I created as well. I will still use what the book has but Ill try to add in some commented code to show the other way to do it.


@doc """
I would just use a setup block to do the same thing for every part of this module
then once inside the describe block we can then leverage the other fixtures. Keep in mind that 

import Pento.{CatalogFixtures, AccountsFixtures, SurveyFixtures]

setup [:register_and_log_in_user]

describe "test" do
    setup [:create_product, etc]

end
"""
describe "Survey Results" do
    setup [:register_and_log_in_user, :create_product, :create_user]

    setup %{user: user, product: product, scope: scope} do
      create_demographic(scope, user)
      create_rating(scope, user, product, 2)
      user2 = user_fixture(@create_user2_attrs)
      scope2 = Accounts.Scope.for_user(user2)
      create_demographic(scope2, user2, @create_demographic_over_18_attrs)
      create_rating(scope2, user2, product, 3)
      :ok
    end
  end

Okay we are ready to write our first test.


Test The Survey Chart Filter

We will first add in a test for the chart within the describe block we just created.

    test "it filters by age group", %{conn: conn} do
        # Test coming soon.  
    end

This is the way we want to describe out tests, but for each test we will need to:

• Mount and render the live view
• Find the age group filter drop down menu and select an item from it
• Assert that the re-rendered survey results chart has the correct data and markup

In order to set up the LiveView we need to use a live/2 function.

    test "it filters by age group", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/admin/dashboard")
    end

As we have talked about before the chart is part of a parent process so we must render the whole page in order to test the chart. Render the parent will render all the children.


There is a way to test just a component you must use LiveViewTest.render_component/2 This will just pull up the markup for that component in order to test the functionality you will need to render the parent.

Simulate and Event

You will need to find the element that you want to interact with in order to trigger the event. If we remember the html for the survey_results_live.ex we know that the id for the age-group filter was called id=”age-group-from” so we can use element/3 in order to trigger an event.

      html =
        view
        |> element("#age-group-form")
        |> IO.inspect()

We added in the IO.inspect/2 so we could see what is returned.

...
#Phoenix.LiveViewTest.Element<
    selector: "#age-group-form",
    text_filter: nil,
    ...
>

Now we need to render a change to the element, with render_change/2. This will trigger a phx-change event. This will then trigger the phx-target to see which component gets the change.

      html =
        view
        |> element("#age-group-form")
        |> render_change(%{"age_group_filter" => "18 and under"})

This will then trigger a param change to the page and that will trigger the handler for that event. In order for us to verify a test we need to know the expected outcome for the events we are triggering. We could do some math and other things to get the exact value we might be looking for but there is an other way.


There is an other LiveViewTest function open_browser/1 this will open up a browser if we invoke it and it will show us the current state of the test. Let’s add that in now and then run the test.

      html =
        view
        |> open_browser()
        |> element("#age-group-form")
        |> render_change(%{"age_group_filter" => "18 and under"})
    end

Then we can run the tests

mix test

Once it is rendered in the browser you can then use the inspect tool to see what is being called for the value of the rating. If you have that you will know what to put for an assert.


That should have opened up a browser that is the view that we might get when we opened the page. Now we need to figure out the ratings that might be the average (or change when we call the open_browser/1). After looking at the code we would see that it should be 2.00 so we can now add in the assert.

    test "it filters by age group", %{conn: conn} do
      {:ok, view, _html} = live(conn, "/admin/dashboard")

      params = %{"age_group_filter" => "18 and under"}

      assert view
             |> element("#age-group-form")
             |> render_change(params) =~ "2.00</text>"
    end

Verify Distributed Realtime Updates (336)

Now we want to start to test the PubSub part of the code to be sure that we are getting the right results. Elixir testing in LiveView can be as simple as using send/2

Set Up the Test

Let’s add a new block to the /test/pento_web/live/admin_dashboard_live_test.exs

    test "it updates to display newly created ratings",
         %{conn: conn, product: product} do
      # coming soon!
    end

If we wanted to see what the current state of the test was we could add in the open_browser/1 and we would see that we currently thanks to the setup block have an average rating of 2.5 for the test game.


Add a Rating

We now also have a way to be sure of the state with an other test to start out the new test. Let’s add that in now.

    test "it updates to display newly created ratings",
         %{conn: conn, product: product} do
      {:ok, view, html} = live(conn, "/admin/dashboard")

      assert html =~ "2.50</text>"
    end

Okay lets add in an other user demo with a rating of 3.0 and then see if the page changes. (This is without the send… it shouldn’t)

    test "it updates to display newly created ratings",
         %{conn: conn, product: product} do
      {:ok, view, html} = live(conn, "/admin/dashboard")

      assert html =~ "2.50</text>"

      user3 = user_fixture(@create_user3_attrs)
      scope3 = Accounts.Scope.for_user(user3)
      create_demographic(scope3, user3, @create_demographic_attrs)
      create_rating(scope3, user3, product, 3)

      assert render(view) =~ "2.67</text>"
    end

Trigger an Interaction with send/2

  1) test Survey Results it updates to display newly created ratings (PentoWeb.AdminDashboardLiveTest)
     test/pento_web/live/admin_dashboard_live_test.exs:49
     Assertion with =~ failed
     code:  assert html =~ "2.67</text>"

There is no send message to let it know to update the page. Let’s add that in now.

    test "it updates to display newly created ratings",
         %{conn: conn, product: product} do
      {:ok, view, html} = live(conn, "/admin/dashboard")

      assert html =~ "2.50</text>"

      user3 = user_fixture(@create_user3_attrs)
      scope3 = Accounts.Scope.for_user(user3)
      create_demographic(scope3, user3, @create_demographic_attrs)
      create_rating(scope3, user3, product, 3)

      send(view.pid, %{event: "rating_created"})
      :timer.sleep(2)
      assert render(view) =~ "2.67</text>"
    end