Testing in Elixir and LiveView
When I first started building web applications, I was focused on getting things up and running quickly, meeting deadlines, shipping features and moving on to the next challenge.I had heard about testing, but it seemed like an optional extra.Why write tests when I could just fix issues as they came up?That mindset changed when I worked on a project that unexpectedly became much bigger than I had ever anticipated.
It began as a small web app with a manageable codebase — just a few forms, basic database operations, and simple user flows. Everything ran smoothly, and I was able to release features without much thought to testing. I figured I could easily catch errors through manual testing, so I didn’t see the need for formal tests.
As the app grew more complex, bugs started creeping in. A minor tweak to one feature would break other parts of the app. I found myself spending more time debugging than building, as the system’s components, real-time updates, and database interactions became harder to manage. Users began experiencing crashes at critical moments, and I was constantly rushing to patch things in production.
That’s when I realized testing was essential. I started with basic unit tests to ensure individual functions worked as expected, then expanded to integration tests. Using Phoenix’s LiveViewTest, I simulated real-time interactions to ensure my live components behaved reliably.
What is Testing and Why is It Important?
Testing is the process of systematically verifying that the various parts of your application perform as expected. In the world of software development, especially when dealing with real-time applications like those built with Phoenix LiveView, testing is essential for several reasons:
- Prevents Bugs: Testing helps catch errors before they make it to production. This is critical when your app has multiple moving components, real-time updates, and complex interactions.
- Ensures Stability: As applications evolve, even small changes can introduce bugs in other parts of the code. Testing helps safeguard against unexpected issues, ensuring that changes don’t break existing features.
- Encourages Refactoring: With a solid test suite, you can confidently refactor code, knowing that your tests will catch any unintended side effects.
- Improves Code Quality: Writing tests forces you to think critically about how your code is structured, often resulting in cleaner, more maintainable code.
- Enhances Confidence: When you deploy, you can be confident that your core functionality works as intended, thanks to comprehensive testing. This reduces stress, especially in production environments.
Liveview Testing
In this guide, we will walk through the basics of LiveView testing using a practical example: a contact form.
Prerequisites
To follow along with this guide, you’ll need to clone the example repository where the contact form is implemented. This will provide you with the codebase necessary for testing. You can find the repository here: contact_form
What Is LiveView Testing?
LiveView testing focuses on verifying the functionality of LiveView components, which are server-rendered views that can update in real time. Testing these components ensures that:
- Real-time updates are handled correctly.
- User interactions produce the expected outcomes.
- UI elements render and behave as intended.
Key Concepts in LiveView Testing
Understanding the key concepts behind LiveView testing is crucial for writing effective and maintainable tests. Here’s a deeper dive into some of the important aspects:
Macros and Imports
use ContactFormWeb.ConnCase
- Purpose: When you use
ContactFormWeb.ConnCase
, you are importing a set of functionalities and configurations that help simulate web requests and interact with your application's components during testing. - Functionality: This macro sets up the necessary dependencies and configurations for testing, such as a connection (
conn
) that simulates HTTP requests to your application. It also provides utility functions for working with connections, which are essential for testing controllers and LiveView components.
2.import Phoenix.LiveViewTest
- Purpose: By importing
Phoenix.LiveViewTest
, you gain access to functions and macros specifically designed for testing Phoenix LiveView components. This import is crucial for interacting with LiveViews and performing actions such as rendering forms and submitting them. - Functions:
live/2
: Opens a LiveView session, allowing you to interact with LiveView components in your tests.form/2
: Simulates filling out and submitting a form within a LiveView.render_submit/1
: Submits the form and captures the result, which you can then assert against expected outcomes.render_click/2
: Simulates clicking an element within the LiveView. Useful for testing interactions that trigger events.
Macros for Testing
describe
- Purpose: The
describe
macro is used to group related tests under a descriptive header. This helps in organizing tests and making your test output easier to understand. - Usage: Inside the
describe
block, you can define multiple test cases that pertain to a specific functionality or feature of your LiveView component.
describe "Contact Form" do
# Test cases go here
end
2.test
- Purpose: The
test
macro defines an individual test case within your test module. Each test case should focus on a specific aspect of the component’s behavior. - Usage: Inside a
test
block, you write assertions to verify that the LiveView component behaves as expected. The test function usually takes a context map (e.g.,%{conn: conn}
) which provides necessary setup for the test.
test "form renders correctly", %{conn: conn} do
{:ok, _live_view, html} = live(conn, Routes.page_index_path(conn, :index))
# Assertions
end
Test Structure
- Test Modules and Files
- Location: In a Phoenix project, test files are located in the
test
directory. Each test file typically corresponds to a specific feature or component of your application.
Example test file path: test/your_app_web/live/page_live_test.exs
defmodule YourAppNameWeb.PageLiveTest do
use YourAppNameWeb.ConnCase
import Phoenix.LiveViewTest
describe "Page LiveView" do
test "renders page correctly", %{conn: conn} do
# assertions
end
end
end
Running Your Tests
To run the tests, use the following command in your terminal:
mix test
Exploring Our Contact Form Tests
Let’s take a closer look at the three tests written for our contact form to understand what each one does:
find the code here
Test 1: The form renders correctly
test "form renders correctly", %{conn: conn} do
# Opens a LiveView session and fetches the HTML of the rendered form
{:ok, _live_view, html} = live(conn, Routes.page_index_path(conn, :index))
# Checks that the HTML contains the expected text for form fields and buttons
assert html =~ "First Name"
assert html =~ "Last Name"
assert html =~ "Email Address"
assert html =~ "Query Type"
assert html =~ "Message"
assert html =~ "I consent to being contacted by the team"
assert html =~ "Submit"
end
- Purpose: This test checks that the contact form displays all the necessary fields correctly.
- Explanation: It simulates opening the contact form and verifies that all expected labels and buttons are present in the rendered HTML.
assert
: This macro is used to check if a certain condition is true. If the condition is not true, the test fails. It's a way to verify that your code is working as expected.=~
: This operator checks if a given string contains a specific substring. It is used withassert
to confirm that certain text is present in the output or response
Test 2: Cannot Submit Form with Invalid Inputs
test "cannot submit form with invalid inputs", %{conn: conn} do
{:ok, live_view, _html} = live(conn, Routes.page_index_path(conn, :index))
# Simulate filling out the form with invalid data
live_view
|> form("#contact-request-form",
client: %{
first_name: "",
last_name: "",
email: "examplegmail.com",
query_type: "General inquiry",
message: "this is a message",
contact_consent: false
}
)
# Submit the form with invalid data
|> render_submit()
# Check that the form displays appropriate error messages
assert render(live_view) =~ "This field is required"
assert render(live_view) =~ "To submit this form, please consent to being contacted"
assert render(live_view) =~ "please enter a valid email address"
end
- Purpose: This test ensures that the form does not submit with invalid or incomplete inputs.
- Explanation: It simulates filling out the form with invalid data, submits it, and verifies that appropriate error messages are shown
Test 3: Submits with Valid Inputs
test "submits with valid inputs", %{conn: conn} do
{:ok, live_view, _html} = live(conn, Routes.page_index_path(conn, :index))
# Simulate filling out the form with valid data
live_view
|> form("#contact-request-form",
client: %{
first_name: "John",
last_name: "Doe",
email: "example@gmail.com",
query_type: "General inquiry",
message: "this is a message",
contact_consent: true
}
)
# Submit the form with valid data
|> render_submit()
# Check that the form displays a success message
assert render(live_view) =~ "Thanks for completing the form,we'll be in touch soon!"
end
- Purpose: This test verifies that the form successfully submits and displays a confirmation message when valid data is provided.
- Explanation: It simulates filling out the form with correct information, submits it, and checks that the success message is rendered.
Testing LiveView components is crucial for ensuring that your real-time features and user interactions perform as expected. By using tools like Phoenix.LiveViewTest
, you can simulate user behavior, validate component rendering, and catch potential issues before they affect your users. This approach helps maintain a seamless and reliable user experience.
Next, we’ll delve into unit testing, where we’ll focus on testing individual functions and modules in isolation to ensure they work correctly and efficiently.