From elixir-phoenix-guide
MANDATORY for ALL test files. Invoke before writing any _test.exs file.
How this skill is triggered — by the user, by Claude, or both
Slash command
/elixir-phoenix-guide:testing-essentialsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
1. **Use `DataCase` for database tests, `ConnCase` for LiveView/controller tests** — never mix them
DataCase for database tests, ConnCase for LiveView/controller tests — never mix themasync: true only when safe — safe: pure functions, changesets, helpers; unsafe: DB contexts with shared rows, LiveView, Application.put_env, external servicestest/support/) — never build it inline across multiple testshas_element?/2 and element/2 for LiveView assertions — not html =~ "text" for structure checksdescribe blocks to group tests by function or behaviorWrite the failing test first. Run it to confirm it fails for the right reason. Implement the minimum code to make it pass. Never write implementation before the test exists.
mix test test/my_app/accounts_test.exs # Should fail first
# ... implement ...
mix test test/my_app/accounts_test.exs # Should pass
defmodule MyApp.AccountsTest do
use MyApp.DataCase, async: true
alias MyApp.Accounts
import MyApp.AccountsFixtures
end
defmodule MyAppWeb.UserLiveTest do
use MyAppWeb.ConnCase, async: true
import Phoenix.LiveViewTest
import MyApp.AccountsFixtures
end
Define all test data in test/support/fixtures/:
defmodule MyApp.AccountsFixtures do
def user_fixture(attrs \\ %{}) do
{:ok, user} =
attrs
|> Enum.into(%{
email: "user#{System.unique_integer([:positive])}@example.com",
password: "hello world!"
})
|> MyApp.Accounts.register_user()
user
end
end
describe "create_post/1" do
test "with valid attrs creates a post" do
assert {:ok, %Post{} = post} = Blog.create_post(%{title: "Hello"})
assert post.title == "Hello"
end
test "with invalid attrs returns error changeset" do
assert {:error, %Ecto.Changeset{} = changeset} = Blog.create_post(%{})
assert %{title: ["can't be blank"]} = errors_on(changeset)
end
end
describe "index" do
test "lists posts", %{conn: conn} do
post = post_fixture()
{:ok, _lv, html} = live(conn, ~p"/posts")
assert html =~ post.title
end
test "unauthorized user is redirected", %{conn: conn} do
{:error, {:redirect, %{to: path}}} = live(conn, ~p"/admin/posts")
assert path == ~p"/login"
end
end
describe "create" do
test "saves post with valid attrs", %{conn: conn} do
{:ok, lv, _html} = live(conn, ~p"/posts/new")
lv
|> form("#post-form", post: %{title: "New Post"})
|> render_submit()
assert has_element?(lv, "p", "Post created")
end
test "shows errors with invalid attrs", %{conn: conn} do
{:ok, lv, _html} = live(conn, ~p"/posts/new")
lv
|> form("#post-form", post: %{title: ""})
|> render_submit()
assert has_element?(lv, "p.alert", "can't be blank")
end
end
describe "changeset/2" do
test "valid attrs" do
assert %Ecto.Changeset{valid?: true} = Post.changeset(%Post{}, %{title: "Hello"})
end
test "requires title" do
changeset = Post.changeset(%Post{}, %{})
assert %{title: ["can't be blank"]} = errors_on(changeset)
end
end
Compose reusable setup functions with setup [:func1, :func2]. Each function receives and returns a context map.
defmodule MyAppWeb.PostLiveTest do
use MyAppWeb.ConnCase, async: true
import MyApp.AccountsFixtures
import MyApp.BlogFixtures
setup [:register_and_log_in_user, :create_post]
test "owner can edit post", %{conn: conn, post: post} do
{:ok, lv, _html} = live(conn, ~p"/posts/#{post}/edit")
assert has_element?(lv, "#post-form")
end
defp create_post(%{user: user}) do
%{post: post_fixture(user_id: user.id)}
end
end
Chain order matters — later functions receive assigns from earlier ones.
Never hardcode dates — use relative timestamps to prevent flaky tests as time passes.
# Bad — breaks after 2026
assert post.published_at == ~U[2026-01-15 12:00:00Z]
# Good — relative to now
now = DateTime.utc_now(:second)
assert DateTime.diff(post.inserted_at, now, :second) < 5
# Good — build relative dates for filtering/sorting
past = DateTime.add(DateTime.utc_now(:second), -7, :day)
future = DateTime.add(DateTime.utc_now(:second), 7, :day)
old_post = post_fixture(published_at: past)
new_post = post_fixture(published_at: future)
assert Blog.list_published_posts() == [old_post]
See testing-guide.md for comprehensive examples covering async tests, Mox mocking, file upload testing, and Ecto query testing.
Guides completion of development work by verifying tests, detecting environment, and presenting structured options for merge, PR, or cleanup.
Enforces test-driven development: write failing test first, then minimal code to pass. Use when implementing features or bugfixes.
Guides creation and editing of skills using test-driven development with pressure scenarios and subagents to verify agent compliance.
npx claudepluginhub roger120981/elixir-phoenix-guide