code-quality
Use when refactoring or reviewing Elixir code quality — compiler warnings and Credo first, then the plugin's on-demand…
Use when writing or fixing ExUnit tests — DataCase/ConnCase, generated fixtures and scopes, LiveView test helpers, uploads, Mox and Req.Test stubs, async and sandbox rules.
$ npx -y skills add j-morgan6/elixir-phoenix-guide --skill testing-essentials --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/testing-essentialsContext preview
The summary Claude sees to decide when to auto-load this skill.
Use when writing or fixing ExUnit tests — DataCase/ConnCase, generated fixtures and scopes, LiveView test helpers, uploads, Mox and Req.Test stubs, async and sandbox rules.
name: testing-essentials description: "Use when writing or fixing ExUnit tests — DataCase/ConnCase, generated fixtures and scopes, LiveView test helpers, uploads, Mox and Req.Test stubs, async and sandbox rules."
> Verified against Elixir 1.20, Phoenix 1.8, LiveView 1.2, Ecto 3.14, Req 0.7 — October 2026
For longer worked examples (a full context + LiveView suite, uploads, Mox wiring, sandbox allowances, StreamData), read the [testing reference](reference.md) when the patterns below aren't enough.
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 2. **Test both happy path AND error/invalid cases** for every function 3. **Prefer `async: true` — it is safe under the Ecto SQL Sandbox**, for DataCase and ConnCase/LiveView tests alike. Opt out only for real shared state: `Application.put_env`, named or app-supervised processes, Mox/Req.Test global mode. (Generators are inconsistent: phx.gen.auth's web tests are async, but its `accounts_test.exs` and the phx.gen.live / phx.gen.context tests aren't — add it.) 4. **Use the generated fixtures and scopes** — `user_fixture/1`, `user_scope_fixture/0`, `post_fixture(scope, attrs)`, and `setup :register_and_log_in_user` (puts `conn`, `user` and `scope` in the context). New fixtures go in `test/support/fixtures/`, never inline across tests 5. **Test authorization both ways** — guests are redirected to `~p"/users/log-in"`, and a record created under one scope can't be read or changed through another 6. **Assert LiveView structure with `has_element?/3`**, not `html =~` on markup. When an event navigates, pipe into `follow_redirect/2` and assert on the page you land on — the old view is gone 7. **Mock only at boundaries** — Mox against a behaviour with `setup :verify_on_exit!`; HTTP through `Req.Test` stubs. Never mock your own contexts or Repo 8. **Never `Process.sleep/1`** — use `assert_receive`, `render_async/2`, `start_supervised!/1` 9. **Test the public context interface**, not private helpers or internal state 10. **Use `describe` blocks** to group tests by function or behavior
---
Write the failing test first, run it, confirm it fails for the right reason, then implement the minimum to pass.
mix test test/my_app/blog_test.exs:42 # one test by line mix test --failed # re-run only last run's failures mix test --stale # only tests affected by changed modules mix test --warnings-as-errors # fail on compile warnings in test files (CI)
---
`DataCase` (context and schema tests) has no `conn`. `ConnCase` (controllers, LiveViews) builds one and checks out the sandbox. Both call `setup_sandbox/1`, which starts a sandbox owner per test — that's why `async: true` is safe.
defmodule MyApp.BlogTest do use MyApp.DataCase, async: true alias MyApp.Blog import MyApp.AccountsFixtures, only: [user_scope_fixture: 0] import MyApp.BlogFixtures end defmodule MyAppWeb.PostLiveTest do use MyAppWeb.ConnCase, async: true import Phoenix.LiveViewTest import MyApp.BlogFixtures end
---
Follow the shape `phx.gen.context` emits with scopes: the scope comes first, attrs merge over defaults, the record goes through the real context function.
defmodule MyApp.BlogFixtures do
def post_fixture(scope, attrs \\ %{}) do
attrs = Enum.into(attrs, %{title: "some title", body: "some body"})
{:ok, post} = MyApp.Blog.create_post(scope, attrs)
post
end
endphx.gen.auth (1.8) registration is email-only. `user_fixture/1` already confirms the user via magic link; use `unconfirmed_user_fixture/1` for the unconfirmed case and `set_password/1` when a test needs a password. When you add a field to users, add it to `valid_user_attributes/1` — don't write a new user fixture.
Fixtures for a custom scope entry (`org_scope_fixture/0`, its `test_setup_helper`) are in the `phoenix-scopes-and-generators` skill.
---
describe "posts" do
test "list_posts/1 returns only the scope's posts" do
scope = user_scope_fixture()
other_scope = user_scope_fixture()
post = post_fixture(scope)
_other = post_fixture(other_scope)
assert Blog.list_posts(scope) == [post]
end
test "get_post!/2 raises for another user's post" do
post = post_fixture(user_scope_fixture())
assert_raise Ecto.NoResultsError, fn -> Blog.get_post!(user_scope_fixture(), post.id) end
end
test "create_post/2 with valid attrs sets the owner from the scope" do
scope = user_scope_fixture()
assert {:ok, %Post{} = post} = Blog.create_post(scope, %{title: "Hello", body: "Hi"})
assert post.user_id == scope.user.id
end
test "create_post/2 with invalid attrs returns an error changeset" do
assert {:error, %Ecto.Changeset{} = changeset} = Blog.create_post(user_scope_fixture(), %{})
assert %{title: ["can't be blank"]} = errors_on(changeset)
end
end---
Mirrors `phx.gen.live`'s `live_test.exs`. `render_change/1` returns HTML, so the apostrophe is escaped (`can't`). A submit that navigates must be followed; the flash is on the new page.
defmodule MyAppWeb.PostLiveTest do
use MyAppWeb.ConnCase, async: true
import Phoenix.LiveViewTest
import MyApp.BlogFixtures
setup :register_and_log_in_user
defp create_post(%{scope: scope}), do: %{post: post_fixture(scope)}
test "redirects guests to log in" do
assert {:error, {:redirect, %{to: path}}} = live(build_conn(), ~p"/posts")
assert path == ~p"/users/log-in"
end
describe "Index" do
setup [:create_post]
test "lists the user's posts", %{conn: conn, post: post} do
{:ok, lv, _html} = live(conn, ~p"/posts")
assert has_element?(· Changelog A Claude Code plugin (with Codex support for the skills) that keeps Claude's Elixir and Phoenix code idiomatic and current.
Use when refactoring or reviewing Elixir code quality — compiler warnings and Credo first, then the plugin's on-demand…
Use when preparing releases or deployment config — mix phx.gen.release, runtime.exs vs compile-time config, release migrations,…
Use when writing Ecto changeset validations beyond cast/validate_required — custom and cross-field validations (e.g. end date…
Use when defining schemas, writing queries, or creating migrations — scoped context functions, Repo.transact, upserts, indexes,…
Use when migrating PostgreSQL tables that serve live traffic — index concurrently,…
Use when one form or operation manages parent and child records together — cast_assoc/cast_embed, inputs_for with sort/drop…