Curated library of 38 atomic skills, 7 personas, and 1 orchestrator for Elixir and Phoenix development. Organized by category: fundamentals, phoenix, database, testing, auth, infrastructure, quality, security, integrations, tooling, frameworks, personas, and orchestration. Covers core Elixir patterns, Phoenix LiveView, Ecto, OTP, Oban, testing, security, deployment, real-time, and modern tooling (Req, Swoosh, Cachex, Broadway, Ash).
91
91%
Does it follow best practices?
Impact
91%
1.37xAverage score across 56 eval scenarios
Advisory
Suggest reviewing before use
async: true only when safe — avoid for DB contexts with shared rows, LiveView, Application.put_env, and external servicestest/support/) — never build it inline across multiple testshas_element?/2 and element/2 for LiveView assertions — not html =~ "text" for structure checksFollow these steps in order, with explicit validation at each checkpoint:
test/support/fixtures/ for relevant fixtures before creating new onesmix test to confirm the fixture compiles before writing any testsmix test path/to/file_test.exs and confirm it fails with a meaningful message (not a compile error)mix test path/to/file_test.exs and confirm greenSee assets/tdd_checklist.md for a copy-paste RED/GREEN/REFACTOR checklist and pre-commit quality gate.
See assets/spec_templates.md for copy-paste DataCase, ConnCase, LiveView, isolated-LiveView, and ChannelCase test templates.
defmodule MyApp.AccountsTest do
use MyApp.DataCase, async: true
alias MyApp.Accounts
import MyApp.AccountsFixtures
enddefmodule MyAppWeb.UserLiveTest do
use MyAppWeb.ConnCase, async: true
import Phoenix.LiveViewTest
import MyApp.AccountsFixtures
endDefine 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
enddescribe "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
enddescribe "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
enddescribe "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
endUse setup [:func1, :func2] to compose reusable setup functions:
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❌ Bad — hardcoded date will eventually be in the past:
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]| Symptom | Fix |
|---|---|
ownership timeout / DBConnection.OwnershipError | Set async: false |
LiveView cannot find ownership process | Set async: false (LiveView tests must not be async) |
Application.put_env leaking between tests | Use async: false; restore original value in on_exit callback |
| Flaky timestamp assertions | Replace hardcoded datetimes with DateTime.diff/3 (see Timestamp Testing above) |
| Unexpected redirect in LiveView | Confirm test user has required role/session via register_and_log_in_user setup |
Do not invoke this skill for: pure-function unit tests with no DB/external side effects (use plain ExUnit), property-based testing (property-based-testing skill), benchmarking (benchee-profiling skill), Mox patterns for external services, or LiveView streams (phoenix/liveview-streams skill).
| ❌ Don't | ✅ Do |
|---|---|
async: true on LiveView or shared-row DB tests | Set async: false when tests share mutable state |
assert html =~ "Save" for structure checks | has_element?(lv, "#post-form button", "Save") |
| Build test data inline in every test | Define reusable fixtures in test/support/ |
assert post.published_at == ~U[2026-01-15 12:00:00Z] | Assert relative to DateTime.utc_now/1 with DateTime.diff/3 |
| Test only the happy path | Always add the unauthorized/invalid-attrs case |
| Reuse a hardcoded email across tests | "user#{System.unique_integer([:positive])}@example.com" |
| Predecessor | This Skill | Successor |
|---|---|---|
| ecto-essentials | testing-essentials | property-based-testing |
| phoenix-liveview-essentials | testing-essentials | code-quality |
Companion skills: property-based-testing, benchee-profiling, tdd, code-quality.
.tessl-plugin
evals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10
scenario-11
scenario-12
scenario-13
scenario-14
scenario-15
scenario-16
scenario-17
scenario-18
scenario-19
scenario-20
scenario-21
scenario-22
scenario-23
scenario-24
scenario-25
scenario-26
scenario-27
scenario-28
scenario-29
scenario-30
scenario-31
scenario-32
scenario-33
scenario-34
scenario-35
scenario-36
scenario-37
scenario-38
scenario-39
scenario-40
scenario-41
scenario-42
scenario-43
scenario-44
scenario-45
scenario-46
scenario-47
scenario-48
scenario-49
scenario-50
scenario-51
scenario-52
scenario-53
scenario-54
scenario-55
scenario-56
skills
frameworks
ash-framework
infrastructure
orchestration
elixir-skill-router
personas
phoenix
quality
security
security-essentials
tooling
mix-tasks-generators