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

31 Likes

Holy heck this is AWESOME! Thank you!!!

2 Likes

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: 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: :point_down:

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

To this: :point_down:

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

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

7 Likes

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.

1 Like

follow_redirect happens automatically, or is there another purpose you need it directly?

I kind of like this as a separate library, at least for the foreseeable future. I’ve always seen LiveViewTest as sort of a “lower level” API that works fine on its own for testing but also as a tool for building libraries like this. PhoenixTest is probably just opinionated enough that it doesn’t really fit the bill to belong in Phoenix as not everyone is going to like. So it leaves room for alternate takes on this type of library. Of course I personally wouldn’t be upset if it were merged :slight_smile:

So, how would that work if I’m testing an auth flow where there can be a couple of redirects from controllers, and I want to ensure its the correct path that is redirected?

While it’s still a bit of a work-in-progress, there is assert_path. Multiple redirect should I think, though I actually haven’t switched any auth over to PhoenixTest myself, yet.

Yeah, it’s not really the same.

I tend to build my own function that do another get on the location header, then I match the html on the response.

Ah I see… I think! I would create an issue in the repo and see if there is anything that can be done.

I ended up with writing a PR to Phoenix: Add follow_redirect/2 to Phoenix.ConnTest by Schultzer · Pull Request #5797 · phoenixframework/phoenix · GitHub

1 Like

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

4 Likes

Hi @germsvel

Using PhoenixTest and really enjoying the experience. I’m trying to use fill_in with a type=‘hidden’ field. I get an error:

 ** (ArgumentError) Found label but can't find labeled element whose `id` matches label's `for` attribute.

Looking at the PhoenixTest code it looks like hidden fields are explicitly excluded. Have I got this right and if so may I ask the reason. Are there any plans to allow hidden fields in future?

many thanks

This is the same behaviour as LiveViewTest.

This function is meant to mimic what the user can actually do, so you cannot set hidden input values. However, hidden values can be given when calling render_submit/2 or render_change/2, see their docs for examples.

Currently submit doesn’t accept these args, though you can unwrap and use Phoenix.LiveViewTest.render_submit etc.

Perfect. Thanks @sodapopcan.

cheers

1 Like

Hey! Is there a way to configure the endpoint for PhoenixTest for multiple apps in an umbrella? Thought I’d ask before I start stabbing the library :sweat_smile:

Thanks a lot!

Really amazing work @germsvel! Looking forward it being part of Phoenix :rocket:

I have one pain point, that I want to share. When I want to fill the form, I’ve had frequently came to the issue of choosing label! e.g:

...
<label>Event Date:</label>
<input id="event_start_datetime" type="dateteime-local" />
<input id="event_end_datetime" type="dateteime-local" />

I tried to set the label as "" and set the exact: false but I get this error

     ** (ArgumentError) Found many labels with text "":
     
     code: |> fill_in("#event_start_datetime", "", with: "2020-09-01T01:01", exact: false)

Also I have taken your Testing LiveView course, and I became big believer of using data-role and query selector in test cases. I’m thinking if relying on the labels which could change by Copy team is sustainable approach for test cases.

Looking forward to get feedback from you!

For people whom have the question, check fill_in without a label · Issue #132 · germsvel/phoenix_test · GitHub

the idea is always have a label, even if it’s not needed and hide it or make it sr-only which increases the page accessibility

Wish they’d consider that idea

3 Likes