Elixir Testing Reference
Ash projects: Use DataCase with Ash.Test helpers; test actions via domain code interfaces, not direct Repo calls. See ash-framework skill.
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:
references/exunit-patterns.md - Setup, assertions, tags
references/mox-patterns.md - Behaviours, expect/stub, async
references/liveview-testing.md - Forms, async, uploads
references/factory-patterns.md - ExMachina, sequences, traits
1---2name: testing3description: Write or repair Elixir tests with ExUnit, sandbox isolation, async; Use for test files, test setup, or failing/flaky…4---56# Elixir Testing Reference78> **Ash projects**: Use `DataCase` with `Ash.Test` helpers; test actions via domain code interfaces, not direct `Repo` calls. See `ash-framework` skill.910Quick reference for Elixir testing patterns.1112## Iron Laws — Never Violate These13141. **ASYNC BY DEFAULT** — Use `async: true` unless tests modify global state152. **SANDBOX ISOLATION** — All database tests use Ecto.Adapters.SQL.Sandbox163. **MOCK ONLY AT BOUNDARIES** — Never mock database, internal modules, or stdlib174. **BEHAVIOURS AS CONTRACTS** — All mocks must implement a defined `@callback` behaviour185. **BUILD BY DEFAULT** — Use `build/2` in factories; `insert/2` only when DB needed196. **NO PROCESS.SLEEP** — Use `assert_receive` with timeout for async operations207. **VERIFY_ON_EXIT!** — Always call in Mox tests setup218. **FACTORIES MATCH SCHEMA REQUIRED FIELDS** — Factory definitions must include all fields that have `validate_required` in the schema changeset. Missing fields cause cascading test failures2223## Quick Decisions2425### Which Test Case?2627| Testing | Use |28|---------|-----|29| Controller/API | `use MyAppWeb.ConnCase` |30| Context/Schema | `use MyApp.DataCase` |31| LiveView | `use MyAppWeb.ConnCase` + `import Phoenix.LiveViewTest` |32| Pure logic | `use ExUnit.Case, async: true` |3334### When to use async: true?3536- ✅ Pure functions, no shared state37- ✅ Database tests with Sandbox (PostgreSQL)38- ❌ Tests modifying `Application.put_env`39- ❌ Tests using Mox global mode4041### Mock or not?4243- ✅ Mock: External APIs, email services, file storage44- ❌ Don't mock: Database, internal modules, stdlib4546### build() or insert()?4748- Use `build()` by default for speed49- Use `insert()` only when you need DB ID, constraints, or persisted associations5051## Quick Patterns5253```elixir54# Setup chain55setup [:create_user, :authenticate]5657# Pattern matching assertion58assert {:ok, %User{name: name}} = create_user(attrs)5960# Async message assertion61assert_receive {:user_created, _}, 50006263# Mox setup64setup :verify_on_exit!65expect(MockAPI, :call, fn _ -> {:ok, "data"} end)6667# LiveView async68html = render_async(view) # MUST call for assign_async69```7071## Common Anti-patterns7273| Wrong | Right |74|-------|-------|75| `Process.sleep(100)` | `assert_receive {:done, _}, 5000` |76| `insert(:user)` in factory | `build(:user)` in factory |77| `async: true` with `set_mox_global()` | `async: false` |78| Mock internal modules | Test through public API |7980## References8182For detailed patterns, see:8384- `references/exunit-patterns.md` - Setup, assertions, tags85- `references/mox-patterns.md` - Behaviours, expect/stub, async86- `references/liveview-testing.md` - Forms, async, uploads87- `references/factory-patterns.md` - ExMachina, sequences, traits