# Elixir Testing

> 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.

- Skill: `andrewdryga/elixir-testing` (Agent Skill)
- Install (CLI): `npx skillmds@latest add andrewdryga/elixir-testing`
- Raw SKILL.md: https://api.skillmd.com/api/skills/andrewdryga/elixir-testing/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: AndrewDryga (https://skillmd.com/u/andrewdryga)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/andrewdryga/elixir-testing

---


# 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/3` 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`.

