From elixir-phoenix-guide
Provides ExUnit test patterns: async safety, fixture definitions, LiveView test helpers, error-path coverage, and TDD workflow for Elixir/Phoenix projects.
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. **Follow the project's existing test setup patterns** (e.g. shared setup helpers like `setup :store_test_session`) — don't inline DataCase/ConnCase boilerplate that the project already abstracts away
setup :store_test_session) — don't inline DataCase/ConnCase boilerplate that the project already abstracts awayasync: true by default; disable it only for real shared state — global state, Application.put_env, named processes, external services. LiveView/ConnCase tests are async-safe under the Ecto SQL Sandbox (Phoenix's own generators emit async: true).test/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]
npx claudepluginhub j-morgan6/elixir-phoenix-guide --plugin elixir-phoenix-guideProvides Elixir testing patterns for ExUnit, Mox mocks, factories, and LiveView helpers. Use on *_test.exs files, test/support/, factories, or fixing test failures.
Guides Elixir testing with ExUnit: unit/integration/property-based tests, assertions, mocks, fixtures, tags, describe blocks, setup, and concurrent code testing.
Guides Elixir testing with ExUnit for unit tests, property-based tests, mocks, and test suite organization.