Phoenix.ConnTest: Testing Phoenix Controllers and Endpoints
Phoenix applications expose their behavior through HTTP endpoints. Testing these endpoints — verifying correct status codes, response bodies, redirects, and authentication — is essential for confident Phoenix development. Phoenix.ConnTest provides exactly this capability, letting you exercise your controllers without starting an actual HTTP server.
What Phoenix.ConnTest Does
Phoenix.ConnTest builds on Plug.Test and gives you a conn-based testing interface. You simulate HTTP requests by building a Plug.Conn struct, passing it through your router, and asserting on the resulting conn.
This is faster than spinning up a real HTTP server and more integrated than unit testing controllers in isolation — you test the full plug pipeline: router, plugs, controller, and view.
Setting Up
Phoenix generates a ConnCase for you in test/support/conn_case.ex:
defmodule MyAppWeb.ConnCase do
use ExUnit.CaseTemplate
using do
quote do
import Plug.Conn
import Phoenix.ConnTest
import MyAppWeb.Router.Helpers
@endpoint MyAppWeb.Endpoint
end
end
setup tags do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(MyApp.Repo)
unless tags[:async] do
Ecto.Adapters.SQL.Sandbox.mode(MyApp.Repo, {:shared, self()})
end
%{conn: build_conn()}
end
endYour test modules use this case:
defmodule MyAppWeb.UserControllerTest do
use MyAppWeb.ConnCase
# ...
endMaking HTTP Requests
The request helpers follow HTTP verbs:
# GET request
conn = get(conn, ~p"/users")
conn = get(conn, ~p"/users/#{user.id}")
# POST with body
conn = post(conn, ~p"/users", %{user: %{name: "Alice", email: "alice@example.com"}})
# PUT/PATCH
conn = put(conn, ~p"/users/#{user.id}", %{user: %{name: "Alice Updated"}})
conn = patch(conn, ~p"/users/#{user.id}", %{user: %{name: "Alice Updated"}})
# DELETE
conn = delete(conn, ~p"/users/#{user.id}")The ~p sigil (Phoenix 1.7+) is a verified route helper — it fails at compile time if the route doesn't exist.
Asserting on Responses
After making a request, assert on the resulting conn:
test "lists all users", %{conn: conn} do
conn = get(conn, ~p"/users")
assert html_response(conn, 200) =~ "Users"
end
test "shows a user", %{conn: conn} do
user = create_user()
conn = get(conn, ~p"/users/#{user.id}")
assert html_response(conn, 200) =~ user.name
end
test "redirects after creation", %{conn: conn} do
conn = post(conn, ~p"/users", %{user: valid_attrs()})
assert redirected_to(conn) == ~p"/users"
end
test "returns 404 for unknown user", %{conn: conn} do
conn = get(conn, ~p"/users/9999")
assert html_response(conn, 404)
endTesting JSON APIs
For API endpoints, use json_response/2:
defmodule MyAppWeb.Api.UserControllerTest do
use MyAppWeb.ConnCase
test "creates user and returns JSON", %{conn: conn} do
conn = post(conn, ~p"/api/users", %{
name: "Alice",
email: "alice@example.com"
})
assert %{"id" => id, "name" => "Alice"} = json_response(conn, 201)
assert is_integer(id)
end
test "returns validation errors as JSON", %{conn: conn} do
conn = post(conn, ~p"/api/users", %{name: ""})
assert %{"errors" => errors} = json_response(conn, 422)
assert errors["email"] == ["can't be blank"]
end
test "lists users as JSON", %{conn: conn} do
insert_list(3, :user)
conn = get(conn, ~p"/api/users")
users = json_response(conn, 200)
assert length(users) == 3
end
endTesting Authentication
Most applications require authentication. The typical pattern is to build an authenticated conn in a helper:
defmodule MyAppWeb.ConnCase do
# ... existing code ...
def log_in_user(conn, user) do
token = MyApp.Accounts.generate_user_session_token(user)
conn
|> Phoenix.ConnTest.init_test_session(%{})
|> Plug.Conn.put_session(:user_token, token)
end
endUse it in tests:
defmodule MyAppWeb.DashboardControllerTest do
use MyAppWeb.ConnCase
describe "GET /dashboard" do
test "requires authentication", %{conn: conn} do
conn = get(conn, ~p"/dashboard")
assert redirected_to(conn) == ~p"/login"
end
test "shows dashboard to authenticated user", %{conn: conn} do
user = create_user()
conn = conn |> log_in_user(user) |> get(~p"/dashboard")
assert html_response(conn, 200) =~ "Dashboard"
end
end
endFor token-based API auth:
defp auth_conn(conn, user) do
token = MyApp.Accounts.create_api_token(user)
put_req_header(conn, "authorization", "Bearer #{token}")
end
test "returns data for authenticated request", %{conn: conn} do
user = create_user()
conn = conn |> auth_conn(user) |> get(~p"/api/profile")
assert %{"email" => _} = json_response(conn, 200)
endTesting Flash Messages
Flash messages signal outcomes to users. Test them directly:
test "shows success flash after creating user", %{conn: conn} do
conn = post(conn, ~p"/users", %{user: valid_attrs()})
assert get_flash(conn, :info) == "User created successfully."
end
test "shows error flash on failure", %{conn: conn} do
conn = post(conn, ~p"/users", %{user: invalid_attrs()})
assert html_response(conn, 200) =~ "Oops, something went wrong"
endTesting File Uploads
For multipart form submissions with file uploads:
test "uploads avatar", %{conn: conn} do
user = create_user()
upload = %Plug.Upload{
path: "test/fixtures/avatar.png",
filename: "avatar.png",
content_type: "image/png"
}
conn =
conn
|> log_in_user(user)
|> put(~p"/users/#{user.id}", %{user: %{avatar: upload}})
assert redirected_to(conn) == ~p"/users/#{user.id}"
endReusing Conn State Between Requests
A fresh conn is given to each test. To simulate a multi-step flow (login, then visit page), chain requests:
test "login and access protected page", %{conn: conn} do
user = create_user(password: "secret")
# Login
conn = post(conn, ~p"/login", %{email: user.email, password: "secret"})
assert redirected_to(conn) == ~p"/dashboard"
# Follow redirect with same conn (preserves session)
conn = get(conn, ~p"/dashboard")
assert html_response(conn, 200) =~ "Welcome"
endEnd-to-End Complement
Controller tests verify your Phoenix logic layer. For validating real browser behavior — JavaScript interactions, LiveView updates, visual layout — HelpMeTest provides browser-based testing that runs your full front-end stack. Controller tests catch backend logic errors early; browser tests catch integration issues users would actually encounter.
Summary
Phoenix.ConnTest gives you a fast, expressive way to test Phoenix controllers:
- Use
get,post,put,patch,deleteto simulate requests - Assert with
html_response,json_response,redirected_to - Build authenticated conns with helper functions
- Test flash messages, uploads, and multi-step flows
- Run tests async with Ecto's SQL sandbox
Controller tests in Phoenix are deliberately fast — no browser, no real HTTP server. That speed lets you run the full suite often without waiting, catching regressions early in the development cycle.