germsvel

germsvel

PhoenixTest - a unified way of writing feature tests for LiveView and static pages

PhoenixTest provides a unified way of writing feature tests – regardless of whether you’re testing LiveView pages or static pages.

It also handles navigation between LiveView and static pages seamlessly. You don’t have to worry about what type of page you’re visiting or navigating to. So, you can test a flow going from static to LiveView pages and back without having to worry about the underlying implementation.

Just write your tests from the user’s perspective. :sparkles:

Why create PhoenixTest?

With the advent of LiveView, I find myself writing less and less JavaScript.

Sure, there are sprinkles of it here and there, and there’s always the occasional need for something more.

But for the most part, if I’m going to build a page that needs interactivity, I use LiveView. If I’m going to write a static page, I use regular controllers + views/HTML modules.

The problem is that LiveView pages and static pages have vastly different testing strategies.

If I use LiveView, I can write my tests like this:

{:ok, view, _html} = live(conn, ~p"/")

html =
  view
  |> element("#greet-guest")
  |> render_click()

assert html =~ "Hello, guest!"

But if I want to test a static page, we have to resort to controller testing:

conn = get(conn, ~p"/greet_page")

assert html_response(conn, 200) =~ "Hello, guest!"

That means I don’t have ways of interacting with static pages at all!

What if I want to submit a form or click a link? And what if a click takes me from a LiveView to a static view or vice versa?

A Unified way of writing feature tests

So, I wanted a unified way of writing feature tests – regardless of whether or not you’re testing LiveView pages or static pages.

And that’s what PhoenixTest does!

Imagine having a workflow that starts on a static page. We click a link and are taken to a LiveView page. Then, we fill a LiveView form (which could even have its phx-change triggered), and then we submit the form. Finally, we assert that what we expect to see is present.

All of that in a succinct, pipe-friendly way!

test "can create a user", %{conn: conn} do
  conn
  |> visit("/")  # <- could be LiveView or static page
  |> click_link("Users") # <- navigate between LiveViews and static pages
  |> fill_form("#user-form", name: "Aragorn", email: "aragorn@dunedain.com")
    # ^ will trigger `phx-change` if present
  |> click_button("Create") # <- submits LiveView forms and regular forms
  |> assert_has(".user", "Aragorn") # <- provides better error messages
end

Improving assertions

Then there’s the problem of assertions.

Because LiveView and controller tests use =~ for assertions, the error messages aren’t very helpful when assertions fail.

After all, we’re just comparing two blobs of text – and trust me, HTML pages can get very large and hard to read as a blob of text in your terminal.

LiveView tries to help with the has_element?/3 helper, which allows us to target elements by CSS selectors and text.

But, unfortunately, it still doesn’t provide the best errors.

has_element?/3 only tells us what was passed into the function. It doesn’t give us a clue as to what else might’ve been on the page – maybe we just made a small typo and we have no idea!

With PhoenixTest.assert_has/3 and PhoenixTest.refute_has/3, we can provide better errors when assertions fail.

How did you write feature tests before?

In the past, I would’ve used Wallaby to write feature tests.

But since I’m not writing as much JavaScript, I don’t feel the need for something so heavy – requiring chromedriver and slowing down our tests.

Instead, I’d like to have something more akin to what LiveView tests brought us – tests that act as though there’s a driver, but they mostly parse HTML and allows us to make assertions about it.

That’s where PhoenixTest shines.

Of course, without something like chromedriver, we cannot test JavaScript. So, PhoenixTest remains blissfully ignorant of it. If you need that, check Wallaby.

But if you’re looking for something to write fast feature tests for your LiveView pages and static pages – and something that is (hopefully) a pleasure to write – check out PhoenixTest!

Links

Most Liked

germsvel

germsvel

The library has been consistently improving since its introduction.

Today I published the biggest change (from an API perspective) on how we fill form.

PhoenixTest v0.2.13 deprecates two form helpers :disappointed_face: but introduces new ones to help us fill forms with labels. :star_struck:

I know it’s a pain to change this (I don’t change APIs lightly), but I think the changes improve our tests.

Change this: :backhand_index_pointing_down:

session
|> fill_form("form", user: %{
  name: "Aragorn",
  admin: true,
  country: "Arnor"
})

To this: :backhand_index_pointing_down:

session
|> fill_in("Name", with: "Aragorn")
|> check("Admin")
|> select("Arnor", from: "Countries")

Read the full upgrade guide: Upgrade Guides — PhoenixTest v0.9.1

germsvel

germsvel

PhoenixTest v0.3.0 is now out! :mega:

  • Removes deprecated code

  • Adds unwrap for an escape hatch :hatching_chick:

  • Handles buttons submitting forms when not nested in the form

  • and more!

Grab it while it’s hot :fire:

:gear: phoenix_test | Hex

:books: PhoenixTest — PhoenixTest v0.3.0

soyjeansoy

soyjeansoy

Wish they’d consider that idea

sodapopcan

sodapopcan

Holy heck this is AWESOME! Thank you!!!

Schultzer

Schultzer

I think follow redirect would be awesome, but even better would be to kinda merge all of this with phoenix and phoenix live_view into phoenix.

Where Next?

Popular in Announcing Top

ityonemo
Currently just starting out on a new mini-project - getting zig NIFs to run in elixir. https://github.com/ityonemo/zigler The idea here...
New
Flo0807
Hello everyone! I am excited to share our heart project Backpex with you. After building several Phoenix applications, we realized that...
New
BartOtten
Phoenix Live Favicon Favicon manipulation for Phoenix Live A lib enabling dynamic favicons in Phoenix Live View applications. To sho...
New
frerich
An application wishing to store larger amounts of data typically has two options for doing so: A new column on some table can be introd...
New
jechol
I’m excited to share FeistelCipher and AshFeistelCipher, PostgreSQL-based libraries that provide encrypted integer IDs using the Feistel ...
New
munksgaard
flakify is an igniter installer that allows you to quickly get a Nix flake-based development shell up and running for your Elixir/Phoenix...
New
rms.mrcs
Hi there :waving_hand: Just dropping by to share PhonixLiveState, a lib I’ve just published. It’s still VERY RAW, but already in a reas...
New
mattmower
I’m pleased to announce that we’ve released DemoGen v0.1.7, a library for creating repeatable demo scenarios in your Ecto-based SaaS appl...
New
zoedsoupe
update (since 2025-07-24) the project got forked and rebranded to anubis-mcp, since i not on CloudWalk anymore and can’t ensure they will...
New
rodloboz
Sifter is a a query filtering library for Ecto. It lets frontend apps send human-readable query strings like: "elixir phoenix status:pu...
New

Other popular topics Top

Qqwy
Update: How to use the Blogs &amp; Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3268 119930 1237
New
shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
977 41022 311
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New

We're in Beta

About us Mission Statement