View Source PhoenixTest

Module Version Hex Docs License

PhoenixTest provides a unified way of writing feature tests -- regardless of whether you're testing LiveView pages or static (non-LiveView) pages.

It also handles navigation between LiveView and static pages seamlessly. So, you don't have to worry about what type of page you're visiting. Just write the tests from the user's perspective.

Thus, you can test a flow going from static to LiveView pages and back without having to worry about the underlying implementation.

This is a sample flow:

test "admin can create a user", %{conn: conn} do
  conn
  |> visit("/")
  |> click_link("Users")
  |> fill_form("#user-form", name: "Aragorn", email: "aragorn@dunedain.com")
  |> click_button("Create")
  |> assert_has(".user", "Aragorn")
end

Note that PhoenixTest does not handle JavaScript. If you're looking for something that supports JavaScript, take a look at Wallaby.

Setup

PhoenixTest requires Phoenix 1.7+ and LiveView 0.20+. It may work with earlier versions, but I have not tested that.

Installation

Add phoenix_test to your list of dependencies in mix.exs:

def deps do
  [
    {:phoenix_test, "~> 0.2.7", only: :test, runtime: false}
  ]
end

Configuration

In config/test.exs specify the endpoint to be used for routing requests:

config :phoenix_test, :endpoint, MyAppWeb.Endpoint

Adding a FeatureCase

PhoenixTest helpers can be included via import PhoenixTest.

But since each test needs a conn struct to get started, you'll likely want to set up a few things before that.

To make that easier, it's helpful to create a FeatureCase module that can be used from your tests (replace MyApp with your app's name):

defmodule MyAppWeb.FeatureCase do
  @moduledoc """
  This module defines the test case to be used by tests that require setting up
  a connection to test feature tests.

  Such tests rely on `PhoenixTest` and also import other functionality to
  make it easier to build common data structures and interact with pages.

  Finally, if the test case interacts with the database, we enable the SQL
  sandbox, so changes done to the database are reverted at the end of every
  test. If you are using PostgreSQL, you can even run database tests
  asynchronously by setting `use MyAppWeb.FeatureCase, async: true`, although
  this option is not recommended for other databases.
  """

  use ExUnit.CaseTemplate

  using do
    quote do
      use MyAppWeb, :verified_routes

      import MyAppWeb.FeatureCase

      import PhoenixTest
    end
  end

  setup tags do
    pid = Ecto.Adapters.SQL.Sandbox.start_owner!(MyApp.Repo, shared: not tags[:async])
    on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)

    {:ok, conn: Phoenix.ConnTest.build_conn()}
  end
end

Note that we assume your Phoenix project is using Ecto and its phenomenal SQL.Sandbox. If it doesn't, feel free to remove the SQL.Sandbox code above.

Usage

Now, you can create your tests like this:

# test/my_app_web/features/admin_can_create_user_test.exs

defmodule MyAppWeb.AdminCanCreateUserTest do
  use MyAppWeb.FeatureCase, async: true

  test "admin can create user", %{conn: conn} do
    conn
    |> visit("/")
    |> click_link("Users")
    |> fill_form("#user-form", name: "Aragorn", email: "aragorn@dunedain.com")
    |> click_button("Create")
    |> assert_has(".user", "Aragorn")
  end

For full documentation, take a look at PhoenixTest module docs.

Why PhoenixTest?

A unified way of writing feature tests

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 we use LiveView, we have a good set of helpers.

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

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

assert html =~ "Hello, guest!"

But if we're testing 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 we don’t have ways of interacting with static pages at all!

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

Instead, I'd like to have a unified way of testing Phoenix apps -- regardless of whether we're testing LiveView pages or static pages.

Improved assertions

And 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 its has_element?/3 helper, which allows us to target elements by CSS selectors and text.

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!

And that's where PhoenixTest comes in! A unified way of writing feature tests and improved assertions where they're needed!

What do you mean by "static" pages?

We use the term static as compared to LiveView pages. Thus, in PhoenixTest's terminology static pages are what is typically known as dynamic, server-rendered pages -- pages that were normal prior to the advent of LiveView and which are sometimes called "dead" views. Thus, we do not mean static in the sense that static-site generators (such as Jekyll, Gatsby, etc.) mean it.

Sponsors

Made possible thanks to:

Elixir Streams