Peace of mind from prototype to production - comprehensive web framework for Elixir
Phoenix provides comprehensive testing utilities for controllers, channels, and integration testing. The testing framework is built on ExUnit with specialized helpers for web applications and real-time features.
Phoenix.ConnTest provides utilities for testing HTTP requests, responses, and controller actions.
defmodule Phoenix.ConnTest do
# Connection building
def build_conn() :: Plug.Conn.t()
def build_conn(atom, binary, term) :: Plug.Conn.t()
# HTTP request functions
def get(Plug.Conn.t(), binary, term) :: Plug.Conn.t()
def post(Plug.Conn.t(), binary, term, keyword) :: Plug.Conn.t()
def put(Plug.Conn.t(), binary, term, keyword) :: Plug.Conn.t()
def patch(Plug.Conn.t(), binary, term, keyword) :: Plug.Conn.t()
def delete(Plug.Conn.t(), binary, term) :: Plug.Conn.t()
def options(Plug.Conn.t(), binary, term) :: Plug.Conn.t()
def head(Plug.Conn.t(), binary, term) :: Plug.Conn.t()
def trace(Plug.Conn.t(), binary, term) :: Plug.Conn.t()
# Request dispatch
def dispatch(Plug.Conn.t(), atom, atom, atom, keyword) :: Plug.Conn.t()
# Response assertions
def response(Plug.Conn.t(), atom | integer) :: binary
def html_response(Plug.Conn.t(), atom | integer) :: binary
def json_response(Plug.Conn.t(), atom | integer) :: map
def text_response(Plug.Conn.t(), atom | integer) :: binary
# Response status
def response_content_type(Plug.Conn.t(), atom) :: binary
def redirected_to(Plug.Conn.t(), integer) :: binary
def redirected_params(Plug.Conn.t()) :: map
# Assertions
def assert_error_sent(atom | integer, (() -> any)) :: any
def assert_redirected_to(Plug.Conn.t(), binary) :: Plug.Conn.t()
# Flash helpers
def clear_flash(Plug.Conn.t()) :: Plug.Conn.t()
def fetch_flash(Plug.Conn.t()) :: Plug.Conn.t()
def put_flash(Plug.Conn.t(), atom, binary) :: Plug.Conn.t()
# Session helpers
def init_test_session(Plug.Conn.t(), map) :: Plug.Conn.t()
def put_req_header(Plug.Conn.t(), binary, binary) :: Plug.Conn.t()
def put_req_cookie(Plug.Conn.t(), binary, binary) :: Plug.Conn.t()
def delete_req_cookie(Plug.Conn.t(), binary, keyword) :: Plug.Conn.t()
# Route testing
def bypass_through(Plug.Conn.t(), atom | [atom]) :: Plug.Conn.t()
enddefmodule MyAppWeb.UserControllerTest do
use MyAppWeb.ConnCase
import MyApp.AccountsFixtures
describe "index" do
test "lists all users", %{conn: conn} do
user = user_fixture()
conn = get(conn, ~p"/users")
assert html_response(conn, 200) =~ "Users"
assert response(conn, 200) =~ user.name
end
end
describe "show user" do
setup [:create_user]
test "displays user when found", %{conn: conn, user: user} do
conn = get(conn, ~p"/users/#{user}")
assert html_response(conn, 200) =~ user.name
assert html_response(conn, 200) =~ user.email
end
test "returns 404 when user not found", %{conn: conn} do
assert_error_sent 404, fn ->
get(conn, ~p"/users/999999")
end
end
end
describe "create user" do
test "redirects to show when data is valid", %{conn: conn} do
create_attrs = %{name: "John Doe", email: "john@example.com"}
conn = post(conn, ~p"/users", user: create_attrs)
assert %{id: id} = redirected_params(conn)
assert redirected_to(conn) == ~p"/users/#{id}"
conn = get(conn, ~p"/users/#{id}")
assert html_response(conn, 200) =~ "John Doe"
end
test "renders errors when data is invalid", %{conn: conn} do
invalid_attrs = %{name: nil, email: "invalid"}
conn = post(conn, ~p"/users", user: invalid_attrs)
assert html_response(conn, 200) =~ "New User"
assert response(conn, 200) =~ "can't be blank"
assert response(conn, 200) =~ "has invalid format"
end
end
describe "JSON API" do
test "creates user and returns JSON", %{conn: conn} do
create_attrs = %{name: "Jane Doe", email: "jane@example.com"}
conn =
conn
|> put_req_header("accept", "application/json")
|> post(~p"/api/users", user: create_attrs)
assert %{"id" => id} = json_response(conn, 201)["data"]
conn = get(conn, ~p"/api/users/#{id}")
data = json_response(conn, 200)["data"]
assert data["name"] == "Jane Doe"
assert data["email"] == "jane@example.com"
end
end
defp create_user(_) do
user = user_fixture()
%{user: user}
end
endPhoenix.ChannelTest provides utilities for testing real-time channel communication.
defmodule Phoenix.ChannelTest do
# Socket management
def connect(atom, map, map) :: {:ok, Phoenix.Socket.t()} | :error
def close(Phoenix.Socket.t()) :: :ok
# Channel operations
def subscribe_and_join(Phoenix.Socket.t(), atom, binary, map) ::
{:ok, map, Phoenix.Socket.t()} | {:error, map}
def subscribe_and_join!(Phoenix.Socket.t(), atom, binary, map) ::
{map, Phoenix.Socket.t()}
def join(Phoenix.Socket.t(), atom, binary, map) ::
{:ok, map, Phoenix.Socket.t()} | {:error, map}
def leave(Phoenix.Socket.t()) :: :ok
# Message operations
def push(Phoenix.Socket.t(), binary, map) :: reference
def broadcast_from!(Phoenix.Socket.t(), binary, map) :: :ok
def broadcast_from(Phoenix.Socket.t(), binary, map) :: :ok
# Assertions
def assert_push(binary, map, timeout \\ 100)
def assert_push(binary, map, timeout)
def refute_push(binary, map, timeout \\ 100)
def assert_reply(reference, atom, map, timeout \\ 100)
def assert_reply(reference, atom, timeout \\ 100)
def refute_reply(reference, atom, timeout \\ 100)
def assert_broadcast(binary, map, timeout \\ 100)
def refute_broadcast(binary, map, timeout \\ 100)
# Socket reference
def socket_ref(Phoenix.Socket.t()) :: reference
enddefmodule MyAppWeb.RoomChannelTest do
use MyAppWeb.ChannelCase
import MyApp.AccountsFixtures
setup do
user = user_fixture()
{:ok, socket} = connect(MyAppWeb.UserSocket, %{user_id: user.id})
%{socket: socket, user: user}
end
describe "joining rooms" do
test "successful join to public room", %{socket: socket} do
{:ok, reply, socket} = subscribe_and_join(socket, MyAppWeb.RoomChannel, "room:lobby")
assert reply == %{}
assert socket.assigns.room_id == "lobby"
end
test "join requires authentication", %{socket: socket} do
{:ok, socket} = connect(MyAppWeb.UserSocket, %{})
assert subscribe_and_join(socket, MyAppWeb.RoomChannel, "room:lobby") == {:error, %{}}
end
test "cannot join private room without permission", %{socket: socket} do
{:error, reply} = subscribe_and_join(socket, MyAppWeb.RoomChannel, "room:private")
assert reply.reason == "unauthorized"
end
end
describe "sending messages" do
setup %{socket: socket} do
{:ok, _, socket} = subscribe_and_join(socket, MyAppWeb.RoomChannel, "room:lobby")
%{socket: socket}
end
test "new_message broadcasts to all subscribers", %{socket: socket} do
push(socket, "new_message", %{"body" => "Hello, World!"})
assert_broadcast "new_message", %{body: "Hello, World!"}
end
test "new_message replies with success", %{socket: socket} do
ref = push(socket, "new_message", %{"body" => "Hello, World!"})
assert_reply ref, :ok, %{status: "sent"}
end
test "empty message returns error", %{socket: socket} do
ref = push(socket, "new_message", %{"body" => ""})
assert_reply ref, :error, %{errors: %{body: "can't be blank"}}
end
end
describe "typing indicators" do
setup %{socket: socket} do
{:ok, _, socket} = subscribe_and_join(socket, MyAppWeb.RoomChannel, "room:lobby")
%{socket: socket}
end
test "typing event broadcasts to others but not sender", %{socket: socket, user: user} do
push(socket, "typing", %{"typing" => true})
refute_push "typing", %{user: user.name, typing: true}
assert_broadcast "typing", %{user: user.name, typing: true}
end
end
describe "presence tracking" do
setup %{socket: socket} do
{:ok, _, socket} = subscribe_and_join(socket, MyAppWeb.RoomChannel, "room:lobby")
%{socket: socket}
end
test "tracks user presence on join", %{socket: socket, user: user} do
# Presence is tracked after join
users = MyApp.Presence.list(socket)
assert Map.has_key?(users, to_string(user.id))
assert users[to_string(user.id)].metas == [%{online_at: inspect(System.system_time(:second))}]
end
test "removes presence on leave", %{socket: socket, user: user} do
# Initially present
users = MyApp.Presence.list(socket)
assert Map.has_key?(users, to_string(user.id))
# Leave channel
leave(socket)
# No longer present
users = MyApp.Presence.list("room:lobby")
refute Map.has_key?(users, to_string(user.id))
end
end
endIntegration tests verify complete workflows across multiple components.
defmodule MyAppWeb.UserRegistrationTest do
use MyAppWeb.ConnCase
import Phoenix.LiveViewTest
import MyApp.AccountsFixtures
describe "user registration flow" do
test "successful registration via HTML form", %{conn: conn} do
# Visit registration page
conn = get(conn, ~p"/users/register")
assert html_response(conn, 200) =~ "Register"
# Submit registration form
conn = post(conn, ~p"/users/register", user: %{
name: "John Doe",
email: "john@example.com",
password: "password123"
})
# Should redirect to welcome page
assert redirected_to(conn) == ~p"/"
# Should be logged in
conn = get(conn, ~p"/")
assert html_response(conn, 200) =~ "Welcome, John!"
# User should exist in database
user = MyApp.Repo.get_by(MyApp.Accounts.User, email: "john@example.com")
assert user.name == "John Doe"
assert Bcrypt.verify_pass("password123", user.password_hash)
end
test "registration with invalid data shows errors", %{conn: conn} do
conn = post(conn, ~p"/users/register", user: %{
name: "",
email: "invalid-email",
password: "123"
})
assert html_response(conn, 200) =~ "Register"
assert response(conn, 200) =~ "can't be blank"
assert response(conn, 200) =~ "has invalid format"
assert response(conn, 200) =~ "should be at least 8 character"
end
end
describe "authentication flow" do
setup do
user = user_fixture()
%{user: user}
end
test "login and logout flow", %{conn: conn, user: user} do
# Login
conn = post(conn, ~p"/users/login", user: %{
email: user.email,
password: "password123"
})
assert redirected_to(conn) == ~p"/"
# Should be authenticated
conn = get(conn, ~p"/users/settings")
assert html_response(conn, 200) =~ "Settings"
# Logout
conn = delete(conn, ~p"/users/logout")
assert redirected_to(conn) == ~p"/"
# Should no longer be authenticated
conn = get(conn, ~p"/users/settings")
assert redirected_to(conn) == ~p"/users/login"
end
end
endPhoenix provides specialized testing for LiveView components.
# Example LiveView test
defmodule MyAppWeb.UserLiveTest do
use MyAppWeb.ConnCase
import Phoenix.LiveViewTest
import MyApp.AccountsFixtures
describe "user management" do
test "lists users with live updates", %{conn: conn} do
user = user_fixture()
{:ok, view, html} = live(conn, ~p"/users")
assert html =~ "Users"
assert html =~ user.name
# Create new user via form
assert view
|> form("#user-form", user: %{name: "New User", email: "new@example.com"})
|> render_submit()
# Should see new user in list
assert render(view) =~ "New User"
end
test "real-time updates from other processes", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/users")
# Create user in separate process
user = user_fixture()
# Should automatically appear in LiveView
assert render(view) =~ user.name
end
end
end# config/test.exs
import Config
config :my_app, MyAppWeb.Endpoint,
http: [ip: {127, 0, 0, 1}, port: 4002],
secret_key_base: "test_secret_key",
server: false
config :my_app, MyApp.Repo,
username: "postgres",
password: "postgres",
hostname: "localhost",
database: "my_app_test#{System.get_env("MIX_TEST_PARTITION")}",
pool: Ecto.Adapters.SQL.Sandbox
config :logger, level: :warning
# Test-specific configuration
config :bcrypt_elixir, :log_rounds, 1 # Fast hashing for tests
config :my_app, :email_backend, MyApp.Email.TestAdapter# test/support/conn_case.ex
defmodule MyAppWeb.ConnCase do
use ExUnit.CaseTemplate
using do
quote do
use MyAppWeb, :verified_routes
import Plug.Conn
import Phoenix.ConnTest
import MyAppWeb.ConnCase
alias MyAppWeb.Router.Helpers, as: Routes
@endpoint MyAppWeb.Endpoint
end
end
setup tags do
MyApp.DataCase.setup_sandbox(tags)
{:ok, conn: Phoenix.ConnTest.build_conn()}
end
end
# test/support/channel_case.ex
defmodule MyAppWeb.ChannelCase do
use ExUnit.CaseTemplate
using do
quote do
import Phoenix.ChannelTest
import MyAppWeb.ChannelCase
@endpoint MyAppWeb.Endpoint
end
end
setup tags do
MyApp.DataCase.setup_sandbox(tags)
:ok
end
end# test/support/fixtures/accounts_fixtures.ex
defmodule MyApp.AccountsFixtures do
@moduledoc """
Test fixtures for accounts context.
"""
def unique_user_email, do: "user#{System.unique_integer()}@example.com"
def valid_user_password, do: "password123"
def user_fixture(attrs \\ %{}) do
attrs = Enum.into(attrs, %{
name: "Test User",
email: unique_user_email(),
password: valid_user_password()
})
{:ok, user} = MyApp.Accounts.create_user(attrs)
user
end
def admin_fixture(attrs \\ %{}) do
user_fixture(Map.put(attrs, :role, :admin))
end
def extract_user_token(fun) do
{:ok, captured_email} = fun.(&"[TOKEN]#{&1}[TOKEN]")
[_, token | _] = String.split(captured_email.text_body, "[TOKEN]")
token
end
end# Async tests for isolated database operations
defmodule MyApp.AccountsTest do
use MyApp.DataCase, async: true
test "create_user/1 with valid data creates user" do
attrs = %{name: "Test User", email: "test@example.com", password: "password123"}
assert {:ok, user} = MyApp.Accounts.create_user(attrs)
assert user.name == "Test User"
assert user.email == "test@example.com"
assert Bcrypt.verify_pass("password123", user.password_hash)
end
end
# Synchronous tests for operations that affect shared state
defmodule MyApp.IntegrationTest do
use MyApp.DataCase, async: false
test "email sending integration" do
user = user_fixture()
MyApp.Accounts.send_welcome_email(user)
assert_delivered_email MyApp.Email.welcome_email(user)
end
end# Using Mox for mocking external services
defmodule MyApp.PaymentTest do
use MyApp.DataCase
import Mox
# Set up mocks
setup :verify_on_exit!
test "processes payment successfully" do
MyApp.PaymentGateway.Mock
|> expect(:charge, fn %{amount: 1000, token: "tok_123"} ->
{:ok, %{id: "ch_123", status: "succeeded"}}
end)
assert {:ok, charge} = MyApp.Payments.process_charge(1000, "tok_123")
assert charge.id == "ch_123"
assert charge.status == "succeeded"
end
endInstall with Tessl CLI
npx tessl i tessl/hex-phoenix