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; Use4 for test files, test setup, or failing/flaky…5---67# Elixir Testing Reference89> **Ash projects**: Use `DataCase` with `Ash.Test` helpers; test actions via domain code interfaces, not direct `Repo` calls. See `ash-framework` skill.1011Quick reference for Elixir testing patterns.1213## Iron Laws — Never Violate These14151. **ASYNC BY DEFAULT** — Use `async: true` unless tests modify global state162. **SANDBOX ISOLATION** — All database tests use Ecto.Adapters.SQL.Sandbox173. **MOCK ONLY AT BOUNDARIES** — Never mock database, internal modules, or stdlib184. **BEHAVIOURS AS CONTRACTS** — All mocks must implement a defined `@callback` behaviour195. **BUILD BY DEFAULT** — Use `build/2` in factories; `insert/2` only when DB needed206. **NO PROCESS.SLEEP** — Use `assert_receive` with timeout for async operations217. **VERIFY_ON_EXIT!** — Always call in Mox tests setup228. **FACTORIES MATCH SCHEMA REQUIRED FIELDS** — Factory definitions must include all fields that have `validate_required` in the schema changeset. Missing fields cause cascading test failures2324## Quick Decisions2526### Which Test Case?2728| Testing | Use |29|---------|-----|30| Controller/API | `use MyAppWeb.ConnCase` |31| Context/Schema | `use MyApp.DataCase` |32| LiveView | `use MyAppWeb.ConnCase` + `import Phoenix.LiveViewTest` |33| Pure logic | `use ExUnit.Case, async: true` |3435### When to use async: true?3637- ✅ Pure functions, no shared state38- ✅ Database tests with Sandbox (PostgreSQL)39- ❌ Tests modifying `Application.put_env`40- ❌ Tests using Mox global mode4142### Mock or not?4344- ✅ Mock: External APIs, email services, file storage45- ❌ Don't mock: Database, internal modules, stdlib4647### build() or insert()?4849- Use `build()` by default for speed50- Use `insert()` only when you need DB ID, constraints, or persisted associations5152## Quick Patterns5354```elixir55# Setup chain56setup [:create_user, :authenticate]5758# Pattern matching assertion59assert {:ok, %User{name: name}} = create_user(attrs)6061# Async message assertion62assert_receive {:user_created, _}, 50006364# Mox setup65setup :verify_on_exit!66expect(MockAPI, :call, fn _ -> {:ok, "data"} end)6768# LiveView async69html = render_async(view) # MUST call for assign_async70```7172## Common Anti-patterns7374| Wrong | Right |75|-------|-------|76| `Process.sleep(100)` | `assert_receive {:done, _}, 5000` |77| `insert(:user)` in factory | `build(:user)` in factory |78| `async: true` with `set_mox_global()` | `async: false` |79| Mock internal modules | Test through public API |8081## References8283For detailed patterns, see:8485- `references/exunit-patterns.md` - Setup, assertions, tags86- `references/mox-patterns.md` - Behaviours, expect/stub, async87- `references/liveview-testing.md` - Forms, async, uploads88- `references/factory-patterns.md` - ExMachina, sequences, traits