In the second part of this two-part series, we'll write an integration test that validates interactions within a single live view, and an integration test that validates the interactions between two separate live views.
You will focus on testing the behavior of the survey results chart filter from the previous post. We'll use the LiveViewTest
module's functions to simulate LiveView connections without a browser. With the help of this module, your tests can mount and render live views, trigger events, and then execute assertions against the rendered view.
That's the whole LiveView lifecycle.
So let's get going and write some interactive LiveView tests!
Testing Interactions within a Live View
Our first integration test focuses on the interactions within a single live view. We'll validate the live view's behavior when a user performs some activity on the page. First off, we're going to take a look at the feature that we'll be testing.
The Feature
The SurveyResultsLive
component mentioned in the previous post is rendered within a parent live view, AdminDashboardLive
, that lives at the /admin-dashboard
route. Here's a refresher of the content displayed by that component:
Here, you see a chart that displays survey results and can be filtered by a given age group.
Our test will simulate a user's visit to /admin-dashboard
, followed by their filter selection of the 18 and under
age group. The test will verify an updated survey results chart that displays product ratings from users in that age group.
Because components run in their parent's processes, we'll focus our tests on the AdminDashboardLive
view. LiveViewTest
helper functions will run our admin dashboard live view and interact with the survey results chart. Along the way, you'll get a taste for the wide variety of interactions that the LiveViewTest
module allows you to test.
Begin by setting up a LiveView test for the AdminDashboardLive
view.
The Test
It's best to segregate unit tests and integration tests into their own modules, so create a new file test/gamestore_web/live/admin_dashboard_live_test.exs
and define the module with some fixtures, like this:
Let's break this down. First, our test module uses the GamestoreWeb.ConnCase
behavior. This lets us route to live views using the test connection by giving our tests access to a context map with a key of :conn
pointing to a value of the test connection. Then, import the LiveViewTest
module to get access to LiveView testing functions.
Lastly, define some fixtures that you'll use to create test data. The nitty-gritty details of those fixture functions aren't important. Just understand that they generate the database records needed to log in users and render the admin dashboard page to display a chart with product ratings.
Now that our module is set up, we'll add a describe
block to encapsulate the feature we're testing—the survey results chart functionality:
Two calls to setup/1
seed the test database with a product, users, demographics, and ratings. One of the two users is in the 18 and under
age group, and the other is in a different age group. Then we create a rating for each user.
We're also using a test helper provided for us by the Phoenix authentication generator—register_and_log_in_user/1
. This function creates a conn
struct with a logged-in user, a necessary step because visiting the /admin-dashboard
route requires an authenticated user.
Now that our setup is complete, define the test:
Before writing the body of our test, make a plan. To test this feature, we need to:
- Mount and render the live view.
- Find the age group drop-down menu and select an item from it.
- Assert that the re-rendered survey results chart has the correct data and markup.
This is the pattern you'll always apply to testing live view features: run the live view, simulate some interaction, then validate the rendered result. This pattern should sound familiar—it neatly matches up to the three-step testing process we've been using so far:
- Set up preconditions
- Provide input
- Validate your expectations
Begin with the first step: mounting and rendering the LiveView. Call the LiveViewTest.live/2
function, which takes in the test conn
and spawns a simulated live view process:
The call to live/2
returns a three-element tuple with :ok
, the LiveView process, and the rendered HTML returned from the live view's call to render/1
. We don't need to access that HTML in this test, so ignore it.
Components run in their parent's process. That means the test must start up the AdminDashboardLive
view rather than rendering just the SurveyResultsLive
component. By spawning the AdminDashboardLive
view, we're also rendering the components of the view.
So, by interacting with the view
variable representing the AdminDashboardLive
process above, we'll interact with elements within the SurveyResultsLive
component and test that it behaves appropriately in response to events. This is the correct way to test LiveView component behavior within a live view page.
The test has a running live view now, so provide your input by selecting the 18 and under
age filter. This will require two steps:
- Find the age group drop-down menu
- Choose an item from it
Use the LiveViewTest.element/3
function to find the age group drop-down on the page.
Assuming the drop-down menu HTML form element has an ID of age-group-filter
, you can target it with element/3
like this:
element/3
returns Phoenix.LiveViewTest.Element
struct that we can pipe into another LiveViewTest
function in order to simulate the selection of an item and submission of the form:
The LiveViewTest.render_change/2
function is one of the functions you'll use to simulate user interactions when testing live views. It takes an argument of the selected element and some params, triggering a phx-change
event. Here, make sure to call render_change/2
with the exact params that would be sent to the live view when the user selects an age group filter in the UI.
This event will trigger the associated handler, invoking the reducers that update our socket, and re-rendering the survey results chart with the filtered product rating info.
With our setup and input in place, we're ready to write our assertions. The call to render_change/2
will return the re-rendered template. Add an assertion that the re-rendered chart displays the correct data by validating the presence of an updated title for the product's average rating:
Once again, the details of our assertion aren't important. Just understand that when the product ratings are filtered by age group, you can expect to see the element on the page: <title>2.00</title>
.
And with that, our first integration test is complete! The LiveViewTest
module provided everything we needed to mount and render a connected live view, target elements within that live view—even elements nested within child components—and assert the state of the view after firing DOM events against those elements.
The test code is clean and elegantly composed with a simple pipeline. All of it is written in Elixir with ExUnit and LiveViewTest
functions—we didn't need to bring in any JavaScript dependencies. As a result, writing our test was a straightforward process. We ended up with a reliable test that runs fast and is easy to read.
This is only a small subset of the LiveViewTest
functions that support LiveView testing, but there are many more LiveViewTest
functions that allow you to send any number of DOM events—blurs, form submissions, live navigation, and more. Learn more about them in the docs.
Before we go, let's write one more integration test; this time, to exercise interactions between live views.
Testing Distributed Updates in LiveView
Testing message passing in a distributed application can be painful, but LiveViewTest
makes it easy to test the PubSub-backed real-time features that you can build into your live views. That is because LiveView tests interact with views via process communication. Since PubSub uses simple Elixir message passing, it's easy to test a live view's ability to handle such messages: use send/2
.
In this section, we'll write an integration test that validates the behavior of the AdminDashboardLive
when it receives a specific message over PubSub.
The Feature
Our AdminDashboardLive
supports the following real-time update feature: when a user anywhere in the world submits a new product rating, then the survey results chart on the admin dashboard updates accordingly, in real-time.
The following code flow backs this:
- When a user submits a product rating, then a PubSub event,
"rating_created"
is broadcast over a topic. - The
AdminDashboardLive
view subscribes to that topic and responds to the event by re-rendering theSurveyResultsLive
component with updated data from the database.
The details of the code aren't important for our purposes today—a high-level understanding is all that's needed to write our test. Let's get started.
The Test
Like in our unit test earlier, you can group similar test cases in a single describe block. We already have a describe
block in our integration test module for "Survey Results"
. Since the test of the real-time update feature also describes the behavior of the SurveyResultsLiveComponent
, we'll add another test case to this same describe block:
This time, our test case retrieves both the test conn
and the product
from the setup context. Use this product to create a new rating for display.
Once again, before filling in the body of our test, make a plan. Follow the same three-step process you used for your earlier integration test:
- Mount and render the connected live view.
- Interact with that live view—in this case, by sending the
"rating_created"
message to the live view. - Re-render the view and verify changes in the resulting HTML.
Start by mounting and rendering the live view with the live/2
function:
Here, we add an intermediate assertion to check the starting state of the product ratings label. Expect to change this value once you create a new rating and send the "rating_created"
message to the live view.
Next up, provide your input (a two-step process). First, create a new rating for the product:
Then, send the "rating_created"
message to the live view:
Here, use a simple send/2
to mimic the code flow of PubSub broadcasting the "rating_creating"
message. Our live view should respond by re-rendering the SurveyResultsLive
component with fresh data from the DB, including the newly created rating. All we need to do now is add our assertion:
And that's it! You:
- Established your initial state by mounting and rendering the live view with the call to
live/2
- Provided some input by creating a new rating record and sending a message to the live view with
send/2
- Validated your expectations by asserting that the re-rendered view had some expected content.
Our three-step LiveView testing procedure neatly applies to both integration tests that exercise internal live view behavior, and tests that validate the interactions between live view processes.
Now for wrapping up.
Wrap Up: Write Robust and Comprehensive LiveView Tests
LiveView empowers you to write robust and comprehensive tests without a huge investment of engineering effort.
In the previous post, we comprised our individual live views from pipelines of single-purpose reducers. This provided opportunities for deep unit testing to quickly cover lots of scenarios and edge cases. You can even use the same elegant reducer pipelines in your tests to verify the behavior of your live view pipelines.
This article showed that the LiveViewTest
module provides all the functionality you need to exercise the full range of LiveView interactions in integration tests. We can use the functions in the LiveViewTest
module to apply the same three-step process that guides all of our tests, making it quick and easy to spin up tests for even complex LiveView interactions.
LiveView is built on top of OTP, and a live view is nothing more than a process. This means you can easily test interactions between live views by relying on simple message passing.
The powerful set of tools in your LiveView testing kit is just one of the many reasons that teams can be so productive in LiveView. You and your team can guarantee comprehensive test coverage for your LiveView applications, ensuring that you move quickly while maintaining bug-free code.
P.S. If you'd like to read Elixir Alchemy posts as soon as they get off the press, subscribe to our Elixir Alchemy newsletter and never miss a single post!