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.

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

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:

# 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:

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

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

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

Asserting Results

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

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:

KeyMeaning
successJobs that completed successfully
failureJobs that raised an error and will be retried
discardJobs that exceeded max attempts and were discarded
cancelledJobs that were cancelled
snoozedJobs that were snoozed for later
queues_not_drainedQueues 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 to be configured.

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

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:

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)