CtrlK
BlogDocsLog inGet started
Tessl Logo

igmarin/elixir-phoenix-skills

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

1.37x
Quality

91%

Does it follow best practices?

Impact

91%

1.37x

Average score across 56 eval scenarios

SecuritybySnyk

Advisory

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

SKILL.mdskills/testing/testing-essentials/

name:
testing-essentials
type:
atomic
tags:
atomic
license:
MIT
description:
MANDATORY for ALL test files. Invoke before writing any _test.exs file. Covers DataCase/ConnCase setup, fixture patterns, LiveView tests, changeset tests, async safety, setup chaining, timestamp testing, and TDD workflow. Trigger words: test, mix test, DataCase, ConnCase, fixture, LiveView test, assert, ExUnit.

Testing Essentials

RULES — Follow these with no exceptions

  1. Follow the project's existing test setup patterns — don't inline DataCase/ConnCase boilerplate that the project already abstracts away
  2. Use async: true only when safe — avoid for DB contexts with shared rows, LiveView, Application.put_env, and external services
  3. Define test data in fixtures (test/support/) — never build it inline across multiple tests
  4. Use has_element?/2 and element/2 for LiveView assertions — not html =~ "text" for structure checks
  5. Always test the unauthorized case for any protected resource
  6. Never hardcode dates — use relative timestamps to prevent flaky tests

Workflow: Writing a New Test File

Follow these steps in order, with explicit validation at each checkpoint:

  1. Check existing fixtures — inspect test/support/fixtures/ for relevant fixtures before creating new ones
  2. Create fixture if needed — add to the appropriate fixtures module (see Fixture Pattern below)
  3. Verify compilation — run mix test to confirm the fixture compiles before writing any tests
  4. Write the failing test — implement the test case; run mix test path/to/file_test.exs and confirm it fails with a meaningful message (not a compile error)
  5. Verify the failure message — the failure should describe a missing behaviour, not a setup problem
  6. Implement the feature
  7. Verify the test passes — re-run mix test path/to/file_test.exs and confirm green

See assets/tdd_checklist.md for a copy-paste RED/GREEN/REFACTOR checklist and pre-commit quality gate.

Test Module Setup

See assets/spec_templates.md for copy-paste DataCase, ConnCase, LiveView, isolated-LiveView, and ChannelCase test templates.

DataCase — for context and schema tests

defmodule MyApp.AccountsTest do
  use MyApp.DataCase, async: true

  alias MyApp.Accounts
  import MyApp.AccountsFixtures
end

ConnCase — for LiveView and controller tests

defmodule MyAppWeb.UserLiveTest do
  use MyAppWeb.ConnCase, async: true

  import Phoenix.LiveViewTest
  import MyApp.AccountsFixtures
end

Fixture Pattern

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

Context Test Skeleton

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

LiveView Test Skeleton

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

Changeset Test Skeleton

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

Setup Chaining

Use 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

Timestamp Testing

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]

Troubleshooting Common Failures

SymptomFix
ownership timeout / DBConnection.OwnershipErrorSet async: false
LiveView cannot find ownership processSet async: false (LiveView tests must not be async)
Application.put_env leaking between testsUse async: false; restore original value in on_exit callback
Flaky timestamp assertionsReplace hardcoded datetimes with DateTime.diff/3 (see Timestamp Testing above)
Unexpected redirect in LiveViewConfirm test user has required role/session via register_and_log_in_user setup

When Not to Use

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

Common Pitfalls

❌ Don't✅ Do
async: true on LiveView or shared-row DB testsSet async: false when tests share mutable state
assert html =~ "Save" for structure checkshas_element?(lv, "#post-form button", "Save")
Build test data inline in every testDefine 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 pathAlways add the unauthorized/invalid-attrs case
Reuse a hardcoded email across tests"user#{System.unique_integer([:positive])}@example.com"

Integration

PredecessorThis SkillSuccessor
ecto-essentialstesting-essentialsproperty-based-testing
phoenix-liveview-essentialstesting-essentialscode-quality

Companion skills: property-based-testing, benchee-profiling, tdd, code-quality.

skills

testing

testing-essentials

.mcp.json

README.md

tile.json