一键导入
testing-essentials
Use when writing or fixing ExUnit tests — case templates, async safety, fixtures, LiveView test helpers, error-path coverage.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Use when writing or fixing ExUnit tests — case templates, async safety, fixtures, LiveView test helpers, error-path coverage.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Use when a form or operation manages parent and child records together — cast_assoc/cast_embed, on_replace, Ecto.Multi across tables, FK cascade design.
Use when writing GenServer, Supervisor, Task, Agent, or Registry code — init/handle_continue, call vs cast, supervision strategies, process naming.
Use when defining schemas, writing queries, or creating migrations — schema design, Repo usage, indexes, query composition.
Use when writing or refactoring core Elixir — pattern matching, case/cond/with, pipes, {:ok, _}/{:error, _} contracts. Baseline style for any .ex/.exs change not covered by a more specific skill.
Use when writing LiveView modules or HEEx templates — mount/handle_params lifecycle, assigns, streams, events, components.
Use when adding logging, metrics, or instrumentation — structured Logger usage, telemetry handler attachment, Ecto/Phoenix telemetry events.
| name | testing-essentials |
| description | Use when writing or fixing ExUnit tests — case templates, async safety, fixtures, LiveView test helpers, error-path coverage. |
| file_patterns | ["**/*_test.exs","**/test/**/*.exs"] |
| auto_suggest | true |
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]