Skill v1.0.0
currentAutomated scan100/100version: "1.0.0" name: testing-essentials description: MANDATORY for ALL test files. Invoke before writing any _test.exs file. file_patterns:
- "**/*_test.exs"
- "/test//*.exs"
auto_suggest: true
Testing Essentials
RULES — Follow these with no exceptions
- Follow the project's existing test setup patterns (e.g. shared setup helpers like
setup :store_test_session) — don't inline DataCase/ConnCase boilerplate that the project already abstracts away - Test both happy path AND error/invalid cases for every function
- Use `async: true` only when safe — safe: pure functions, changesets, helpers; unsafe: DB contexts with shared rows, LiveView,
Application.put_env, external services - Define test data in fixtures (
test/support/) — never build it inline across multiple tests - Use `has_element?/2` and `element/2` for LiveView assertions — not
html =~ "text"for structure checks - Always test the unauthorized case for any protected resource
- Test the public context interface, not internal implementation details
- Use `describe` blocks to group tests by function or behavior
TDD Workflow
Write the failing test first. Run it to confirm it fails for the right reason. Implement the minimum code to make it pass. Never write implementation before the test exists.
mix test test/my_app/accounts_test.exs # Should fail first# ... implement ...mix test test/my_app/accounts_test.exs # Should pass
Test Module Setup
DataCase — for context and schema tests
defmodule MyApp.AccountsTest douse MyApp.DataCase, async: truealias MyApp.Accountsimport MyApp.AccountsFixturesend
ConnCase — for LiveView and controller tests
defmodule MyAppWeb.UserLiveTest douse MyAppWeb.ConnCase, async: trueimport Phoenix.LiveViewTestimport MyApp.AccountsFixturesend
Fixture Pattern
Define all test data in test/support/fixtures/:
defmodule MyApp.AccountsFixtures dodef user_fixture(attrs \\ %{}) do{:ok, user} =attrs|> Enum.into(%{email: "user#{System.unique_integer([:positive])}@example.com",password: "hello world!"})|> MyApp.Accounts.register_user()userendend
Context Test Skeleton
describe "create_post/1" dotest "with valid attrs creates a post" doassert {:ok, %Post{} = post} = Blog.create_post(%{title: "Hello"})assert post.title == "Hello"endtest "with invalid attrs returns error changeset" doassert {:error, %Ecto.Changeset{} = changeset} = Blog.create_post(%{})assert %{title: ["can't be blank"]} = errors_on(changeset)endend
LiveView Test Skeleton
describe "index" dotest "lists posts", %{conn: conn} dopost = post_fixture(){:ok, _lv, html} = live(conn, ~p"/posts")assert html =~ post.titleendtest "unauthorized user is redirected", %{conn: conn} do{:error, {:redirect, %{to: path}}} = live(conn, ~p"/admin/posts")assert path == ~p"/login"endenddescribe "create" dotest "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")endtest "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")endend
Changeset Test Skeleton
describe "changeset/2" dotest "valid attrs" doassert %Ecto.Changeset{valid?: true} = Post.changeset(%Post{}, %{title: "Hello"})endtest "requires title" dochangeset = Post.changeset(%Post{}, %{})assert %{title: ["can't be blank"]} = errors_on(changeset)endend
Setup Chaining
Compose reusable setup functions with setup [:func1, :func2]. Each function receives and returns a context map.
defmodule MyAppWeb.PostLiveTest douse MyAppWeb.ConnCase, async: trueimport MyApp.AccountsFixturesimport MyApp.BlogFixturessetup [: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")enddefp create_post(%{user: user}) do%{post: post_fixture(user_id: user.id)}endend
Chain order matters — later functions receive assigns from earlier ones.
Timestamp Testing
Never hardcode dates — use relative timestamps to prevent flaky tests as time passes.
# Bad — breaks after 2026assert post.published_at == ~U[2026-01-15 12:00:00Z]# Good — relative to nownow = DateTime.utc_now(:second)assert DateTime.diff(post.inserted_at, now, :second) < 5# Good — build relative dates for filtering/sortingpast = 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]
See testing-guide.md for comprehensive examples covering async tests, Mox mocking, file upload testing, and Ecto query testing.