agentleFS
igmarinigmarin/elixir-phoenix-skills/skills/testing-essentials/SKILL.md

testing-essentials

Skill4 starsChanged 4 months agoMITView on GitHub

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.

The file

We split this skill into the rules it gives an agent

4 rules. About 0 are common, 1 is distinctive, and 3 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 3

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Prefer unit-testing pure core without the database.

    line 32
  2. Distinctive:

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

    line 53
  3. Very specific:

    Use setup [:func1, :func2] to compose reusable setup functions:

    line 189
  4. Very specific:

    Do not invoke this skill for: pure-function unit tests with no DB/external side effects (use plain ExUnit), property-based testing …

    line 248

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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

FCIS at this boundary

Prefer unit-testing pure core without the database. Use DataCase for true persistence; use plain ExUnit for pure modules.

❌ Bad: every pure calculation goes through Repo

test "discount" do
  order = insert(:order)
  assert Orders.apply_discount(order.id, 10).total == 90
end

✅ Good: pure core test is side-effect free

test "discount" do
  assert Pricing.with_discount(100, 10) == 90
end

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

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.