Write or improve ExUnit tests for portal/ the emisar way — DataCase/ConnCase, the real fixtures in test/support, and the mandatory happy / denial / cross-account paths (§7). Use when adding tests for a context/job/LiveView, or when a change is missing its denial + cross-account coverage.
Installs into .claude/skills of the current project.
Are you the author of Elixir Testing?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/andrewdryga-elixir-testing)
---
name: elixir-testing
description: Write or improve ExUnit tests for portal/ the emisar way — DataCase/ConnCase, the real fixtures in test/support, and the mandatory happy / denial / cross-account paths (§7). Use when adding tests for a context/job/LiveView, or when a change is missing its denial + cross-account coverage.
effort: medium
argument-hint: "<context / worker / module to test>"
allowed-tools: Read, Grep, Glob, Bash, Write, Edit
---
# Tests (the emisar way)
This project uses **plain ExUnit + Ecto sandbox + hand-written fixtures**. There is
**no Mox, no ExMachina, no StreamData** in the dep stack — do not introduce them or
write tests that assume them (`/tooling-verify-api`: test with what's here). Read a sibling
test (`test/emisar/runs_test.exs`, `policies_test.exs`) and match it.
## Setup
- Domain: `use Emisar.DataCase, async: true`. Web: `use EmisarWeb.ConnCase`.
- Fixtures: the per-domain `Fixtures.*` modules under `test/support/fixtures/`;
`Fixtures.Subjects.owner_subject/1` and `subject_for/2` build a **real
`%Subject{}`**, not a stub. Add a fixture there if the context needs one; don't
inline ad-hoc setup that duplicates an existing fixture.
## The three paths (non-negotiable — §7, IL-3)
Every context function covers:
1. **Happy** — the authorized subject gets `{:ok, …}`.
2. **Denial** — a role *without* the permission gets `{:error, :unauthorized}`. A
write isn't done without this.
3. **Cross-account** — account A's subject cannot see/touch account B's row:
`{:error, :not_found}`.
## Doubling third-party calls (no Mox)
Per IL-19, vendor APIs (Paddle, mailer) are wrapped behind a project module. Test by
swapping/configuring that wrapper, or with a config flag — the codebase already uses
the `notify_approvers_async?`-style flag to make async side effects synchronous in
test. Follow that pattern; don't reach for a mocking library.
## Concurrency
- `async: true` is the default — keep it. Anything that spawns a DB-touching process
must inherit `$callers` or be made synchronous in test (the config-flag pattern).
- **No `Process.sleep`** for synchronization — `assert_receive {…}, 500` across
process boundaries.
## Jobs / LiveView
- Jobs: call `JobModule.execute([])` or `execute(keyword_config)` directly. Cover
the idempotency path (run twice → second is a no-op), plus pagination/batches for sweeps.
- LiveView: `Phoenix.LiveViewTest` (`live/2`, `render_click`, `render_submit`); assert
the authorized + denied event paths (IL-15).
## Run
Use `./run test portal <path>` (or `<path>:<line>`) from the repository root for
focused feedback, including the denial/cross-account cases. A focused pass is
not completion: the lead runs the final gate required by `portal/AGENTS.md`.