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.
In this repository34 other files it gives its agents.
- AGENTS.mdAGENTS.md
- apply-phoenix-controller-conventionsSkill
- ash-frameworkSkill
- benchee-profilingSkill
- broadway-data-pipelinesSkill
- cachex-cachingSkill
- code-qualitySkill
- code-reviewSkill
- deployment-gotchasSkill
- ecto-changeset-patternsSkill
- ecto-essentialsSkill
- ecto-migrationSkill
- ecto-nested-associationsSkill
- elixir-essentialsSkill
- gettext-i18nSkill
- liveview-streamsSkill
- mix-tasks-generatorsSkill
- oban-essentialsSkill
- otp-essentialsSkill
- phoenix-authenticationSkill
- phoenix-authorization-patternsSkill
- phoenix-channels-essentialsSkill
- phoenix-json-apiSkill
- phoenix-liveview-essentialsSkill
- phoenix-pubsub-patternsSkill
- phoenix-uploadsSkill
- property-based-testingSkill
- refactor-codeSkill
- req-http-clientSkill
- respond-to-reviewSkill
- security-essentialsSkill
- swoosh-emailsSkill
- telemetry-essentialsSkill
- typespec-dialyzerSkill
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
- Very specific: line 32
Prefer unit-testing pure core without the database.
- Distinctive: line 53
Follow these steps in order, with explicit validation at each checkpoint:
- Very specific: line 189
Use setup [:func1, :func2] to compose reusable setup functions:
- Very specific: line 248
Do not invoke this skill for: pure-function unit tests with no DB/external side effects (use plain ExUnit), property-based testing …
≈ “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.
Files
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
endWorkflow: Writing a New Test File
Follow these steps in order, with explicit validation at each checkpoint:
- Check existing fixtures — inspect
test/support/fixtures/for relevant fixtures before creating new ones - Create fixture if needed — add to the appropriate fixtures module (see Fixture Pattern below)
- Verify compilation — run
mix testto confirm the fixture compiles before writing any tests - Write the failing test — implement the test case; run
mix test path/to/file_test.exsand confirm it fails with a meaningful message (not a compile error) - Verify the failure message — the failure should describe a missing behaviour, not a setup problem
- Implement the feature
- Verify the test passes — re-run
mix test path/to/file_test.exsand 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
endConnCase — for LiveView and controller tests
defmodule MyAppWeb.UserLiveTest do
use MyAppWeb.ConnCase, async: true
import Phoenix.LiveViewTest
import MyApp.AccountsFixtures
endFixture 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
endContext 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
endLiveView 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
endChangeset 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
endSetup 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
endTimestamp 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 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 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.
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
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.

