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: :manualMake 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
endThe 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:
| 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 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:
- Arrange - Create the records and conditions that should match your trigger's
wherefilter - Act - Call
AshOban.Test.schedule_and_run_triggers/2 - 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
endLower-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
endRunning 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)