documentation/topics/testing.md

# Testing

AshOban provides the `AshOban.Test` module for testing triggers and scheduled actions. The core idea is simple: schedule and drain Oban queues synchronously in your test so you can assert on the side effects.

## Configuration

Set Oban to manual testing mode in your test config. This prevents jobs from running automatically and lets you control execution in tests.

```elixir
# config/test.exs
config :my_app, Oban, testing: :manual
```

Make sure the queues used by your triggers are listed in your Oban config (in any environment config or your runtime config). The default queue name for a trigger is the resource's short name joined with the trigger name, e.g. a `:process` trigger on `MyApp.Invoice` uses the queue `:invoice_process`.

## Basic Usage

The primary testing function is `AshOban.Test.schedule_and_run_triggers/2`. It schedules trigger jobs and drains the queues synchronously, returning a map of job outcomes.

```elixir
test "unprocessed records get processed" do
  record =
    MyApp.Invoice
    |> Ash.Changeset.for_create(:create, %{status: :pending})
    |> Ash.create!()

  assert %{success: 2} =
    AshOban.Test.schedule_and_run_triggers({MyApp.Invoice, :process})

  assert Ash.reload!(record).status == :processed
end
```

The `success: 2` count above reflects two successful jobs: one for the **scheduler** (which finds matching records and enqueues worker jobs) and one for the **worker** (which runs the action on the record).

## What to Pass

You can pass various things to `schedule_and_run_triggers/2`:

```elixir
# A specific trigger on a resource (most common in tests)
AshOban.Test.schedule_and_run_triggers({MyApp.Invoice, :send_reminder})

# All triggers on a resource
AshOban.Test.schedule_and_run_triggers(MyApp.Invoice)

# All triggers across a domain
AshOban.Test.schedule_and_run_triggers(MyApp.Billing)

# All triggers for an OTP app
AshOban.Test.schedule_and_run_triggers(:my_app)

# A list of any of the above
AshOban.Test.schedule_and_run_triggers([MyApp.Invoice, {MyApp.Order, :fulfill}])
```

Targeting a specific `{resource, trigger_name}` tuple is recommended in tests for clarity and to avoid running unrelated triggers.

## Testing Scheduled Actions

Scheduled actions are **not** included by default. Pass `scheduled_actions?: true` to include them:

```elixir
AshOban.Test.schedule_and_run_triggers(MyApp.Report, scheduled_actions?: true)
```

Or target one by name with a tuple (this automatically includes it):

```elixir
AshOban.Test.schedule_and_run_triggers({MyApp.Report, :generate_daily_summary})
```

## Asserting Results

The return value is a map you can pattern match on:

```elixir
assert %{success: 3, failure: 0} =
  AshOban.Test.schedule_and_run_triggers(MyApp.Invoice)

# Assert that a job was discarded (e.g. max attempts exceeded)
assert %{discard: 1, success: 1} =
  AshOban.Test.schedule_and_run_triggers({MyApp.Invoice, :fail_example})
```

The keys in the result map are:

| Key | Meaning |
|---|---|
| `success` | Jobs that completed successfully |
| `failure` | Jobs that raised an error and will be retried |
| `discard` | Jobs that exceeded max attempts and were discarded |
| `cancelled` | Jobs that were cancelled |
| `snoozed` | Jobs that were snoozed for later |
| `queues_not_drained` | Queues that were not drained (when `drain_queues?: false`) |

## Testing with Actors

If your triggers run authorized actions, you can pass an actor. This requires an [actor persister](/documentation/tutorials/getting-started-with-ash-oban.md#persisting-the-actor-along-with-a-job) to be configured.

```elixir
AshOban.Test.schedule_and_run_triggers({MyApp.Invoice, :process},
  actor: %MyApp.Accounts.User{id: 1}
)
```

## Testing with Multitenancy

No special configuration is needed. If your triggers are tenant-aware (using `list_tenants` or `use_tenant_from_record?`), they will automatically scope to the correct tenant during testing just as they do in production.

## Typical Test Structure

A full test typically follows this pattern:

1. **Arrange** - Create the records and conditions that should match your trigger's `where` filter
2. **Act** - Call `AshOban.Test.schedule_and_run_triggers/2`
3. **Assert** - Check that the expected side effects occurred

```elixir
defmodule MyApp.Invoice.SendReminderTest do
  use MyApp.DataCase, async: true

  test "sends reminder for unpaid invoices older than 7 days" do
    # Arrange: create an old unpaid invoice
    invoice = create_invoice(status: :unpaid, inserted_days_ago: 10)

    # Act: run the trigger
    AshOban.Test.schedule_and_run_triggers({MyApp.Invoice, :send_reminder})

    # Assert: check the side effect
    assert_email_sent_to(invoice.customer_email)
  end

  test "does not send reminder for recent invoices" do
    # Arrange: create a recent unpaid invoice
    _invoice = create_invoice(status: :unpaid, inserted_days_ago: 1)

    # Act
    AshOban.Test.schedule_and_run_triggers({MyApp.Invoice, :send_reminder})

    # Assert: no email sent
    refute_any_email_sent()
  end

  test "does not send reminder for paid invoices" do
    # Arrange
    _invoice = create_invoice(status: :paid, inserted_days_ago: 10)

    # Act
    AshOban.Test.schedule_and_run_triggers({MyApp.Invoice, :send_reminder})

    # Assert
    refute_any_email_sent()
  end
end
```

## Lower-Level Testing

For more granular control, you can use `Oban.Testing` directly alongside AshOban's scheduling functions.

### Inspecting Enqueued Jobs

```elixir
use Oban.Testing, repo: MyApp.Repo

test "extra args are set on trigger jobs" do
  invoice = create_invoice(status: :unpaid)

  # Schedule without draining
  AshOban.schedule(MyApp.Invoice, :process)

  # Inspect the scheduler job
  assert [_scheduler] =
    all_enqueued(worker: MyApp.Invoice.AshOban.Scheduler.Process)

  # Drain the scheduler queue to create worker jobs
  Oban.drain_queue(queue: :invoice_process)

  # Inspect the worker job
  assert [job] =
    all_enqueued(worker: MyApp.Invoice.AshOban.Worker.Process)

  assert job.args["primary_key"]["id"] == invoice.id
end
```

### Running a Trigger for a Specific Record

Use `AshOban.run_trigger/3` to enqueue a job for a specific record without going through the scheduler:

```elixir
record = create_invoice(status: :unpaid)

AshOban.run_trigger(record, :process,
  action_arguments: %{notify: true},
  actor: %MyApp.User{id: 1}
)

# Then drain queues to execute it
AshOban.Test.schedule_and_run_triggers(MyApp.Invoice)
```