home / skills / j-morgan6 / elixir-phoenix-guide / testing-essentials

testing-essentials skill

/skills/testing-essentials

This skill helps teams enforce Elixir testing best practices by providing structured guidance, fixtures, and test structure rules before writing tests.

npx playbooks add skill j-morgan6/elixir-phoenix-guide --skill testing-essentials

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
3.7 KB
---
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

1. **Use `DataCase` for database tests, `ConnCase` for LiveView/controller tests** — never mix them
2. **Test both happy path AND error/invalid cases** for every function
3. **Use `async: true` only when tests don't touch shared state** or external services
4. **Define test data in fixtures** (`test/support/`) — never build it inline across multiple tests
5. **Use `has_element?/2` and `element/2` for LiveView assertions** — not `html =~ "text"` for structure checks
6. **Always test the unauthorized case** for any protected resource
7. **Test the public context interface**, not internal implementation details
8. **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.

```bash
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

```elixir
defmodule MyApp.AccountsTest do
  use MyApp.DataCase, async: true

  alias MyApp.Accounts
  import MyApp.AccountsFixtures
end
```

### ConnCase — for LiveView and controller tests

```elixir
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/`:

```elixir
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

```elixir
describe "create_post/1" do
  test "with valid attrs creates a post" do
    assert {:ok, %Post{title: "Hello"}} = Blog.create_post(%{title: "Hello"})
  end

  test "with invalid attrs returns error changeset" do
    assert {:error, %Ecto.Changeset{}} = Blog.create_post(%{})
  end
end
```

---

## LiveView Test Skeleton

```elixir
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

```elixir
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
```

---

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

Overview

This skill enforces a mandatory testing boilerplate and workflow for every _test.exs file. It codifies test structure, fixture usage, and LiveView/controller conventions so tests are consistent, maintainable, and safe to run concurrently. Use it as the first step before writing any test file to avoid common pitfalls and missing checks.

How this skill works

Invoke the skill before creating or editing any test file; it validates that the test module uses the appropriate Case (DataCase or ConnCase), imports fixtures and helpers, and follows TDD and fixture patterns. It also guides LiveView assertions to use element helpers, enforces unauthorized-path checks, and ensures both happy-path and error-path tests exist. The skill produces a minimal, consistent test skeleton and reminders for async, fixtures, and describe grouping.

When to use it

  • Before creating any _test.exs file
  • When starting TDD for a new feature or context
  • When adding LiveView or controller tests
  • When writing tests that touch the database or shared state
  • When establishing consistent fixtures and test structure

Best practices

  • Always pick DataCase for context/schema tests and ConnCase for LiveView/controllers — never mix them
  • Write the failing test first, run it, then implement the minimum to pass (TDD)
  • Put all test data in fixtures under test/support/fixtures and reuse them across tests
  • Use async: true only when tests do not touch shared state or external services
  • Assert LiveView structure with has_element?/2 and element/2 rather than string matching HTML
  • Group tests with describe blocks and always include unauthorized/error cases

Example use cases

  • Create a new accounts context: run a failing test using DataCase, implement minimal code to pass
  • Add LiveView form tests: use ConnCase, import LiveView test helpers, and assert with has_element?/2
  • Test changeset validations: assert valid? and use errors_on to verify required fields
  • Protect admin routes: write a test that verifies unauthorized users are redirected to login
  • Refactor fixtures into test/support/fixtures and update tests to use the shared factory functions

FAQ

Do I ever mix DataCase and ConnCase in the same test module?

No. Use DataCase for pure context/schema tests and ConnCase for LiveView or controller tests; mixing them increases complexity and incorrect setup.

When is async: true safe to use?

Use async: true only for tests that do not touch shared state (database tables without sandbox transactions) or external services. If tests interact with shared resources, run them synchronously.

Should I assert HTML with =~ for LiveView tests?

Avoid =~ for structure checks. Use LiveView helpers like has_element?/2 and element/2 to assert presence and structure reliably.