Elixir Testing Reference
Quick reference for Elixir testing patterns.
Iron Laws — Never Violate These
- ASYNC BY DEFAULT — Use
async: true unless tests modify global state
- SANDBOX ISOLATION — All database tests use Ecto.Adapters.SQL.Sandbox
- MOCK ONLY AT BOUNDARIES — Never mock database, internal modules, or stdlib
- BEHAVIOURS AS CONTRACTS — All mocks must implement a defined
@callback behaviour
- BUILD BY DEFAULT — Use
build/2 in factories; insert/2 only when DB needed
- NO PROCESS.SLEEP — Use
assert_receive with timeout for async operations
- VERIFY_ON_EXIT! — Always call in Mox tests setup
- FACTORIES MATCH SCHEMA REQUIRED FIELDS — Factory definitions must include all fields that have
validate_required in the schema changeset. Missing fields cause cascading test failures
Quick Decisions
Which Test Case?
| Testing |
Use |
| Controller/API |
use MyAppWeb.ConnCase |
| Context/Schema |
use MyApp.DataCase |
| LiveView |
use MyAppWeb.ConnCase + import Phoenix.LiveViewTest |
| Pure logic |
use ExUnit.Case, async: true |
When to use async: true?
- ✅ Pure functions, no shared state
- ✅ Database tests with Sandbox (PostgreSQL)
- ❌ Tests modifying
Application.put_env
- ❌ Tests using Mox global mode
Mock or not?
- ✅ Mock: External APIs, email services, file storage
- ❌ Don't mock: Database, internal modules, stdlib
build() or insert()?
- Use
build() by default for speed
- Use
insert() only when you need DB ID, constraints, or persisted associations
Quick Patterns
# Setup chain
setup [:create_user, :authenticate]
# Pattern matching assertion
assert {:ok, %User{name: name}} = create_user(attrs)
# Async message assertion
assert_receive {:user_created, _}, 5000
# Mox setup
setup :verify_on_exit!
expect(MockAPI, :call, fn _ -> {:ok, "data"} end)
# LiveView async
html = render_async(view) # MUST call for assign_async
Common Anti-patterns
| Wrong |
Right |
Process.sleep(100) |
assert_receive {:done, _}, 5000 |
insert(:user) in factory |
build(:user) in factory |
async: true with set_mox_global() |
async: false |
| Mock internal modules |
Test through public API |
References
For detailed patterns, see:
${CLAUDE_SKILL_DIR}/references/exunit-patterns.md - Setup, assertions, tags
${CLAUDE_SKILL_DIR}/references/mox-patterns.md - Behaviours, expect/stub, async
${CLAUDE_SKILL_DIR}/references/liveview-testing.md - Forms, async, uploads
${CLAUDE_SKILL_DIR}/references/factory-patterns.md - ExMachina, sequences, traits
Converted and distributed by TomeVault — claim your Tome and manage your conversions.
1---2name: testing-733description: Elixir testing patterns — ExUnit, Mox, factories, LiveView test helpers. Use when working on *_test.exs, test/support/, factory files, or fixing test failures. Use when this capability is needed.4---56# Elixir Testing Reference78Quick reference for Elixir testing patterns.910## Iron Laws — Never Violate These11121. **ASYNC BY DEFAULT** — Use `async: true` unless tests modify global state132. **SANDBOX ISOLATION** — All database tests use Ecto.Adapters.SQL.Sandbox143. **MOCK ONLY AT BOUNDARIES** — Never mock database, internal modules, or stdlib154. **BEHAVIOURS AS CONTRACTS** — All mocks must implement a defined `@callback` behaviour165. **BUILD BY DEFAULT** — Use `build/2` in factories; `insert/2` only when DB needed176. **NO PROCESS.SLEEP** — Use `assert_receive` with timeout for async operations187. **VERIFY_ON_EXIT!** — Always call in Mox tests setup198. **FACTORIES MATCH SCHEMA REQUIRED FIELDS** — Factory definitions must include all fields that have `validate_required` in the schema changeset. Missing fields cause cascading test failures2021## Quick Decisions2223### Which Test Case?2425| Testing | Use |26|---------|-----|27| Controller/API | `use MyAppWeb.ConnCase` |28| Context/Schema | `use MyApp.DataCase` |29| LiveView | `use MyAppWeb.ConnCase` + `import Phoenix.LiveViewTest` |30| Pure logic | `use ExUnit.Case, async: true` |3132### When to use async: true?3334- ✅ Pure functions, no shared state35- ✅ Database tests with Sandbox (PostgreSQL)36- ❌ Tests modifying `Application.put_env`37- ❌ Tests using Mox global mode3839### Mock or not?4041- ✅ Mock: External APIs, email services, file storage42- ❌ Don't mock: Database, internal modules, stdlib4344### build() or insert()?4546- Use `build()` by default for speed47- Use `insert()` only when you need DB ID, constraints, or persisted associations4849## Quick Patterns5051```elixir52# Setup chain53setup [:create_user, :authenticate]5455# Pattern matching assertion56assert {:ok, %User{name: name}} = create_user(attrs)5758# Async message assertion59assert_receive {:user_created, _}, 50006061# Mox setup62setup :verify_on_exit!63expect(MockAPI, :call, fn _ -> {:ok, "data"} end)6465# LiveView async66html = render_async(view) # MUST call for assign_async67```6869## Common Anti-patterns7071| Wrong | Right |72|-------|-------|73| `Process.sleep(100)` | `assert_receive {:done, _}, 5000` |74| `insert(:user)` in factory | `build(:user)` in factory |75| `async: true` with `set_mox_global()` | `async: false` |76| Mock internal modules | Test through public API |7778## References7980For detailed patterns, see:8182- `${CLAUDE_SKILL_DIR}/references/exunit-patterns.md` - Setup, assertions, tags83- `${CLAUDE_SKILL_DIR}/references/mox-patterns.md` - Behaviours, expect/stub, async84- `${CLAUDE_SKILL_DIR}/references/liveview-testing.md` - Forms, async, uploads85- `${CLAUDE_SKILL_DIR}/references/factory-patterns.md` - ExMachina, sequences, traits8687---88> Converted and distributed by [TomeVault](https://tomevault.io/claim/oliver-kriska) — claim your Tome and manage your conversions.89<!-- tomevault:4.0:skill_md:2026-04-11 -->