Testing
Phoenix testing — controllers, LiveView, channels, and context tests with ExUnit and ConnCase
Phoenix includes a robust testing setup using ExUnit with custom test cases for controllers, LiveViews, and channels.
Test structure
test/
├── test_helper.exs
├── support/
│ ├── conn_case.ex
│ ├── data_case.ex
│ └── channel_case.ex
├── my_app_web/
│ ├── controllers/
│ │ └── post_controller_test.exs
│ ├── live/
│ │ └── post_live_test.exs
│ └── channels/
│ └── room_channel_test.exs
└── my_app/
└── blog_test.exs # context testsController tests (ConnCase)
defmodule MyAppWeb.PostControllerTest do
use MyAppWeb.ConnCase
describe "GET /posts" do
test "lists all posts", %{conn: conn} do
post = BlogFixtures.post_fixture()
conn = get(conn, ~p"/posts")
assert html_response(conn, 200) =~ post.title
end
end
describe "POST /posts" do
test "creates a post and redirects", %{conn: conn} do
conn = post(conn, ~p"/posts", post: %{title: "Hello", body: "World"})
assert redirected_to(conn) == ~p"/posts"
end
end
describe "GET /posts/:id" do
test "shows a post", %{conn: conn} do
post = BlogFixtures.post_fixture()
conn = get(conn, ~p"/posts/#{post}")
assert html_response(conn, 200) =~ post.title
end
end
endConnCase helpers
| Function | Description |
|---|---|
get(conn, path) |
GET request |
post(conn, path, params) |
POST request |
put(conn, path, params) |
PUT request |
patch(conn, path, params) |
PATCH request |
delete(conn, path) |
DELETE request |
html_response(conn, status) |
Assert HTML response with status |
json_response(conn, status) |
Assert JSON response with status |
redirected_to(conn) |
Get redirect target URL |
response(conn, status) |
Get response body |
Authenticated requests
setup %{conn: conn} do
user = AccountsFixtures.user_fixture()
conn = log_in_user(conn, user)
{:ok, conn: conn, user: user}
end
def log_in_user(conn, user) do
conn
|> Phoenix.ConnTest.init_test_session(%{})
|> Plug.Conn.put_session(:user_token, Accounts.generate_user_session_token(user))
endLiveView tests
defmodule MyAppWeb.PostLiveTest do
use MyAppWeb.ConnCase
import Phoenix.LiveViewTest
describe "Index" do
test "lists all posts", %{conn: conn} do
post = BlogFixtures.post_fixture()
{:ok, _index_live, html} = live(conn, ~p"/posts")
assert html =~ post.title
end
test "creates new post", %{conn: conn} do
{:ok, index_live, _html} = live(conn, ~p"/posts")
assert index_live |> element("a", "New Post") |> render_click() =~
"New Post"
assert_patch(index_live, ~p"/posts/new")
index_live
|> form("#post-form", post: %{title: "Test", body: "Body"})
|> render_submit()
assert_patch(index_live, ~p"/posts")
html = render(index_live)
assert html =~ "Test"
end
test "deletes post", %{conn: conn} do
post = BlogFixtures.post_fixture()
{:ok, index_live, _html} = live(conn, ~p"/posts")
assert index_live |> element("#post-#{post.id} a", "Delete") |> render_click()
refute has_element?(index_live, "#post-#{post.id}")
end
end
describe "Show" do
test "displays post", %{conn: conn} do
post = BlogFixtures.post_fixture()
{:ok, _show_live, html} = live(conn, ~p"/posts/#{post}")
assert html =~ post.title
end
end
endLiveView test helpers
| Function | Description |
|---|---|
live(conn, path) |
Connect to a LiveView |
render(live_view) |
Get current HTML |
element(lv, selector) |
Select an element |
render_click(element) |
Click an element |
render_submit(form) |
Submit a form |
render_change(form, params) |
Trigger form change |
form(lv, selector, params) |
Find and fill a form |
assert_patch(lv, path) |
Assert navigation |
assert_redirect(lv, path) |
Assert redirect |
has_element?(lv, selector) |
Check element exists |
follow_redirect(conn, lv) |
Follow a redirect |
Context tests (DataCase)
defmodule MyApp.BlogTest do
use MyApp.DataCase
describe "posts" do
test "create_post/1 with valid data creates a post" do
attrs = %{title: "Test Post", body: "Some body text"}
assert {:ok, %Post{} = post} = Blog.create_post(attrs)
assert post.title == "Test Post"
end
test "create_post/1 with invalid data returns error changeset" do
attrs = %{title: nil}
assert {:error, %Ecto.Changeset{} = changeset} = Blog.create_post(attrs)
assert "can't be blank" in errors_on(changeset).title
end
test "list_posts/0 returns all posts" do
post = BlogFixtures.post_fixture()
assert Blog.list_posts() == [post]
end
test "update_post/2 with valid data updates the post" do
post = BlogFixtures.post_fixture()
attrs = %{title: "Updated"}
assert {:ok, %Post{} = post} = Blog.update_post(post, attrs)
assert post.title == "Updated"
end
test "delete_post/1 deletes the post" do
post = BlogFixtures.post_fixture()
assert {:ok, %Post{}} = Blog.delete_post(post)
assert_raise Ecto.NoResultsError, fn -> Blog.get_post!(post.id) end
end
end
endChannel tests
defmodule MyAppWeb.RoomChannelTest do
use MyAppWeb.ChannelCase
setup do
user = AccountsFixtures.user_fixture()
{:ok, _, socket} =
MyAppWeb.UserSocket
|> socket("user_socket:#{user.id}", %{user_id: user.id})
|> subscribe_and_join(MyAppWeb.RoomChannel, "rooms:42")
{:ok, socket: socket, user: user}
end
test "joining a room", %{socket: socket} do
assert_push "welcome", %{}
end
test "new_msg broadcasts to room", %{socket: socket} do
push(socket, "new_msg", %{"body" => "hello"})
assert_broadcast "new_msg", %{"body" => "hello"}
end
test "broadcasts are pushed to the client", %{socket: socket} do
broadcast_from!(socket, "new_msg", %{"body" => "system"})
assert_push "new_msg", %{"body" => "system"}
end
endChannel test helpers
| Function | Description |
|---|---|
subscribe_and_join(socket, channel, topic) |
Join a channel |
subscribe_and_join!(socket, channel, topic) |
Join (raises on error) |
push(socket, event, payload) |
Push a client event |
broadcast_from(socket, event, payload) |
Broadcast (excluding self) |
broadcast_from!(socket, event, payload) |
Broadcast (excluding self, raises) |
assert_push(event, payload) |
Assert client received push |
assert_broadcast(event, payload) |
Assert broadcast was sent |
refute_push(event, payload) |
Refute push was received |
refute_broadcast(event, payload) |
Refute broadcast was sent |
Fixtures
Fixtures provide test data factories:
# test/support/fixtures/blog_fixtures.ex
defmodule MyApp.BlogFixtures do
def post_fixture(attrs \\ %{}) do
attrs = Enum.into(attrs, %{
title: "Test Post #{System.unique_integer()}",
body: "Some body text"
})
{:ok, post} = MyApp.Blog.create_post(attrs)
post
end
def unique_user_email, do: "user#{System.unique_integer()}@example.com"
def user_fixture(attrs \\ %{}) do
attrs = Enum.into(attrs, %{
email: unique_user_email(),
password: "password123"
})
{:ok, user} = MyApp.Accounts.register_user(attrs)
user
end
endRunning tests
mix test # Run all tests
mix test test/web/ # Run a directory
mix test test/web/post_test.exs # Run a file
mix test test/web/post_test.exs:42 # Run a specific line
mix test --only tag_name # Run tagged tests
mix test --trace # Verbose output
mix test --max-cases 1 # Serial execution
mix test --slowest 10 # Show 10 slowest tests
MIX_ENV=test mix test # Explicit test environment