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: testing-33description: Write or repair Elixir tests with ExUnit, sandbox isolation, async reliability,4 Mox, ExMachina, and LiveViewTest. Use for test files, test setup, or failing/flaky5 tests. NOT for investigating an application bug outside the test suite.6---78# Elixir Testing Reference910> **Ash projects**: Use `DataCase` with `Ash.Test` helpers; test actions via domain code interfaces, not direct `Repo` calls. See `ash-framework` skill.1112Quick reference for Elixir testing patterns.1314## Iron Laws — Never Violate These15161. **ASYNC BY DEFAULT** — Use `async: true` unless tests modify global state172. **SANDBOX ISOLATION** — All database tests use Ecto.Adapters.SQL.Sandbox183. **MOCK ONLY AT BOUNDARIES** — Never mock database, internal modules, or stdlib194. **BEHAVIOURS AS CONTRACTS** — All mocks must implement a defined `@callback` behaviour205. **BUILD BY DEFAULT** — Use `build/2` in factories; `insert/2` only when DB needed216. **NO PROCESS.SLEEP** — Use `assert_receive` with timeout for async operations227. **VERIFY_ON_EXIT!** — Always call in Mox tests setup238. **FACTORIES MATCH SCHEMA REQUIRED FIELDS** — Factory definitions must include all fields that have `validate_required` in the schema changeset. Missing fields cause cascading test failures2425## Quick Decisions2627### Which Test Case?2829| Testing | Use |30|---------|-----|31| Controller/API | `use MyAppWeb.ConnCase` |32| Context/Schema | `use MyApp.DataCase` |33| LiveView | `use MyAppWeb.ConnCase` + `import Phoenix.LiveViewTest` |34| Pure logic | `use ExUnit.Case, async: true` |3536### When to use async: true?3738- ✅ Pure functions, no shared state39- ✅ Database tests with Sandbox (PostgreSQL)40- ❌ Tests modifying `Application.put_env`41- ❌ Tests using Mox global mode4243### Mock or not?4445- ✅ Mock: External APIs, email services, file storage46- ❌ Don't mock: Database, internal modules, stdlib4748### build() or insert()?4950- Use `build()` by default for speed51- Use `insert()` only when you need DB ID, constraints, or persisted associations5253## Quick Patterns5455```elixir56# Setup chain57setup [:create_user, :authenticate]5859# Pattern matching assertion60assert {:ok, %User{name: name}} = create_user(attrs)6162# Async message assertion63assert_receive {:user_created, _}, 50006465# Mox setup66setup :verify_on_exit!67expect(MockAPI, :call, fn _ -> {:ok, "data"} end)6869# LiveView async70html = render_async(view) # MUST call for assign_async71```7273## Common Anti-patterns7475| Wrong | Right |76|-------|-------|77| `Process.sleep(100)` | `assert_receive {:done, _}, 5000` |78| `insert(:user)` in factory | `build(:user)` in factory |79| `async: true` with `set_mox_global()` | `async: false` |80| Mock internal modules | Test through public API |8182## References8384For detailed patterns, see:8586- `references/exunit-patterns.md` - Setup, assertions, tags87- `references/mox-patterns.md` - Behaviours, expect/stub, async88- `references/liveview-testing.md` - Forms, async, uploads89- `references/factory-patterns.md` - ExMachina, sequences, traits