Skip to content
Development
Skill

/testing-essentials

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.

BOOST
From plugin
elixir-phoenix-guide
16924 skills3 hooks
Install
$ npx -y skills add j-morgan6/elixir-phoenix-guide --skill testing-essentials --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/testing-essentials

Context 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.

SKILL.md

testing-essentials.SKILL.md
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."

Testing Essentials

> 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.

RULES — Follow these with no exceptions

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

---

TDD Workflow

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)

---

Case Templates

`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

---

Fixtures

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
end

phx.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.

---

Context Test Skeleton

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

---

LiveView Test Skeleton

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?(
Read more
Ships withelixir-phoenix-guide

· Changelog A Claude Code plugin (with Codex support for the skills) that keeps Claude's Elixir and Phoenix code idiomatic and current.

Get the whole plugin

Other skills on elixir-phoenix-guide.