igmarin/elixir-phoenix-skills

testing-essentials

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.

First seen Jun 20, 2026

Installation

$ npx skills add igmarin/elixir-phoenix-skills --skill testing-essentials

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from igmarin/elixir-phoenix-skills · top by installs.

npx skills add igmarin/elixir-phoenix-skills

Browse all from igmarin/elixir-phoenix-skills

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 2
License LICENSE
Default branch main
Open issues 0
Status Active

Skill metadata

Parsed from SKILL.md frontmatter.

Version1.0.0
LicenseMIT
More metadata
version
1.0.0
user-invocable
true

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 8,578 B
  • docs SUMMARY.md 338 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 18 installs

SKILL.md

Testing Essentials

Canonical FP bar: [docs/fcis-engineering-rules.md](../../docs/fcis-engineering-rules.md) — Functional Core, Imperative Shell: pure domain modules; side effects at edges. Prefer testing pure core without Repo; use DataCase only for true persistence boundaries.

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.putenv, and external services 3. Define test data in fixtures (test/support/) — never build it inline across multiple tests 4. Use haselement?/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/tddchecklist.md](assets/tddchecklist.md) for a copy-paste RED/GREEN/REFACTOR checklist and pre-commit quality gate.

Test Module Setup

See [assets/spectemplates.md](assets/spectemplates.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

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 registerandloginuser 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 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"

Integration

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.