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

73

Quality

91%

Does it follow best practices?

Impact

No eval scenarios have been run

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.
metadata:
{"user-invocable":"true","version":"1.0.0","adapted-from":"j-morgan6/elixir-phoenix-guide","original-author":"Joseph Morgan"}

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

Test Module Setup

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; later functions receive assigns from earlier ones.

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

  • Sandbox ownership errors (ownership timeout or DBConnection.OwnershipError): flip the test to async: false.
  • LiveView sandbox errors (cannot find ownership process): LiveView tests must use async: false.
  • Application.put_env leaking between tests: restore in an on_exit callback and use async: false.
  • Flaky timestamp assertions: replace hardcoded datetimes with DateTime.diff/3 comparisons (see Timestamp Testing above).
  • Unexpected redirect in LiveView: confirm the test user has the required role/session via register_and_log_in_user setup.

See agents/testing-guide.md for comprehensive examples covering async tests, Mox mocking, file upload testing, and Ecto query testing.


Integration

PredecessorThis SkillSuccessor
elixir-essentialstesting-essentialsNone (standalone)

When Not to Use

  • Do not invoke this skill for unit tests of pure functions that have no side effects, DB calls, or external dependencies — write plain ExUnit tests without DataCase/ConnCase
  • Do not invoke this skill for property-based testing — use property-based-testing instead
  • Do not invoke this skill for benchmarking/profiling — use benchee-profiling instead
  • Do not use this skill when you need to mock external services — use property-based-testing skill's Mox patterns instead
  • Do not invoke this skill for testing LiveView streams — use phoenix/liveview-streams skill instead

skills

testing

testing-essentials

README.md

tile.json