Testing#
Since Oban orchestrates your application’s background tasks, testing Oban is highly recommended. Automated testing is essential for building reliable applications. This guide covers unit testing workers, integration testing with queues, and managing test data.
Tip
Because enqueueing jobs, processing jobs, and asserting on test data are async actions, tests
must also be async.
Test Configuration#
For integration testing with queues, an Oban instance must be initialized (but not started), so
it is running in client-only mode. This prevents Oban from automatically processing jobs in the
background, while allowing you to enqueue job as needed.
Typically, your app will initialize Oban when it boots. If you’re testing in isolation, without the app running, then you’ll want to start oban as a fixture:
import pytest
from oban import Oban
@pytest.fixture
async def oban():
pool = await Oban.create_pool()
oban = Oban(pool=pool)
async with oban:
yield oban
await pool.close()
Testing Modes#
Oban provides two testing modes controlled via the oban.testing.mode() context manager:
inline — Jobs execute immediately within the calling process without touching the database. Simple and suitable for most tests.
manual — Jobs are inserted into the database where they can be verified and executed when desired. More flexible but requires database interaction.
By default, jobs are inserted into the database in tests (manual mode). You can switch modes within a test using the context manager:
import oban.testing
async def test_with_inline_mode():
with oban.testing.mode("inline"):
# Job executes immediately without hitting the database
result = await send_email.enqueue("[email protected]", "Hello")
assert result.state == "completed"
Unit Testing Workers#
Worker modules are the primary “unit” of an Oban system. You should test worker logic locally, in-process, without having Oban touch the database.
Testing Process Methods#
The process_job() helper simplifies unit testing by handling boilerplate like JSON
encoding/decoding and setting required job fields.
Let’s test a worker that activates user accounts:
from oban import worker
from oban.testing import process_job
@worker(queue="default")
class ActivationWorker:
async def process(self, job):
user = await User.activate(job.args["id"])
return {"activated": user.email}
async def test_activating_a_new_user():
user = await User.create(email="[email protected]")
job = ActivationWorker.new({"id": user.id})
result = await process_job(job)
assert result["activated"] == "[email protected]"
The process_job() helper:
Converts
argsto JSON and back to ensure valid typesSets required fields like
id,attempt,attempted_atExecutes the worker’s
process()methodReturns the result for assertions
Testing Function Workers#
Function-based workers created with @job can be tested the same way:
from oban import job
from oban.testing import process_job
@job(queue="default")
async def send_notification(user_id: int, message: str):
await NotificationService.send(user_id, message)
return {"sent": True}
async def test_send_notification():
job = send_notification.new(123, "Hello World")
result = await process_job(job)
assert result["sent"] is True
Integration Testing with Queues#
For integration tests, you’ll want to verify that jobs are enqueued correctly and can execute them
when needed. The oban.testing module provides helpers for these scenarios.
Asserting Enqueued Jobs#
Use assert_enqueued() to verify a job was enqueued:
from oban.testing import assert_enqueued
async def test_signup_enqueues_activation(app):
await app.post("/signup", json={"email": "[email protected]"})
await assert_enqueued(
worker=ActivationWorker,
args={"email": "[email protected]"},
queue="default"
)
You can assert on partial args without matching exact values:
async def test_enqueued_args_have_email_key():
await Account.notify_owners(account)
# Match jobs that have an "email" key, regardless of value
await assert_enqueued(queue="default", args={"email": "[email protected]"})
Refuting Enqueued Jobs#
Use refute_enqueued() to assert no matching job was enqueued:
from oban.testing import refute_enqueued
async def test_invalid_signup_skips_activation(app):
response = await app.post("/signup", json={"email": "invalid"})
assert response.status_code == 400
await refute_enqueued(worker=ActivationWorker)
Asserting Multiple Jobs#
For complex assertions on multiple jobs, use all_enqueued():
from oban.testing import all_enqueued
async def test_notifies_all_owners():
await Account.notify_owners(account_with_3_owners)
jobs = await all_enqueued(worker=NotificationWorker)
assert len(jobs) == 3
# Make complex assertions on each job's args
for job in jobs:
assert "email" in job.args
assert "name" in job.args
Waiting for Async Jobs#
If your application enqueues jobs asynchronously, use the timeout parameter:
async def test_async_job_enqueued():
# Trigger async operation that enqueues a job
await trigger_background_process()
# Wait up to 0.2 seconds for the job to be enqueued
await assert_enqueued(worker=ProcessWorker, timeout=0.2)
Executing Jobs in Tests#
Sometimes you need to actually execute jobs during integration tests. Use drain_queue() to
process all pending jobs in a queue:
from oban.testing import drain_queue
async def test_email_delivery():
await Business.schedule_meeting({"email": "[email protected]"})
result = await drain_queue(queue="mailers")
assert result["completed"] == 1
assert result["discarded"] == 0
# Now assert that the email was sent
assert len(sent_emails) == 1
The drain_queue() function supports several options:
# Drain without scheduled jobs
await drain_queue(queue="default", with_scheduled=False)
# Drain without recursion (jobs that enqueue other jobs won't run)
await drain_queue(queue="default", with_recursion=False)
# Drain with safety (errors are recorded, not raised)
await drain_queue(queue="default", with_safety=True)
Resetting Between Tests#
Use reset_oban() to clean up job data between tests:
import pytest
from oban.testing import reset_oban
@pytest.fixture(autouse=True)
async def _reset_oban():
yield
await reset_oban()
Choose the appropriate strategy based on what you’re testing. Use unit tests for worker logic, integration tests with assertions for enqueueing verification, and draining for end-to-end scenarios.