guides/testing/testing_workers.md
Worker modules are the primary "unit" of an Oban system. You can (and should) test a worker's callback functions locally, in-process, without touching the database.
Most worker callback functions take a single argument: an Oban.Job struct. A
job encapsulates arguments, metadata, and other options. Creating jobs, and
verifying that they're built correctly, requires some boilerplate...that's where
Oban.Testing.perform_job/3 comes in!
The perform_job/3 helper reduces boilerplate when constructing jobs for unit
tests and checks for common pitfalls. For example, it automatically converts
args to string keys before calling perform/1, ensuring that perform clauses
aren't erroneously trying to match on atom keys.
Let's work through test-driving a worker to demonstrate.
Start by defining a test that creates a user and then use perform_job to
manually call an account activation worker. In this context "activation" could
mean sending an email, notifying administrators, or any number of
business-critical functions—what's important is how we're testing it.
defmodule MyApp.ActivationWorkerTest do
use MyApp.Case, async: true
test "activating a new user" do
user = MyApp.User.create(email: "[email protected]")
{:ok, _user} = perform_job(MyApp.ActivationWorker, %{id: user.id})
end
end
Running the test at this point will raise an error that explains the module
doesn't implement the Oban.Worker behaviour.
1) test activating a new account (MyApp.ActivationWorkerTest)
Expected worker to be a module that implements the Oban.Worker behaviour, got:
MyApp.ActivationWorker
code: {:ok, user} = perform_job(MyApp.ActivationWorker, %{id: user.id})
To fix it, define a worker module with the appropriate signature and return value:
defmodule MyApp.ActivationWorker do
use Oban.Worker
@impl Worker
def perform(%Job{args: %{"id" => user_id}}) do
MyApp.Account.activate(user_id)
end
end
The perform_job/3 helper's errors will guide you through implementing a
complete worker with the following assertions:
Oban.Worker behaviourargs is encodable/decodable to JSON and always has string keys:ok, {:ok, value}, {:error, value} etc.new/1,2 callback works properlyIf all of the assertions pass, then you'll get the result of perform/1 for you
to make additional assertions on.
You may wish to test less-frequently used worker callbacks such as backoff/1
and timeout/1, but those callbacks don't have dedicated testing helpers.
Never fear, it's adequate to build a job struct and test callbacks directly!
Here's a sample test that asserts the backoff value is simply two-times the
job's attempt:
test "calculating custom backoff as a multiple of job attempts" do
assert 2 == MyWorker.backoff(%Oban.Job{attempt: 1})
assert 4 == MyWorker.backoff(%Oban.Job{attempt: 2})
assert 6 == MyWorker.backoff(%Oban.Job{attempt: 3})
end
Similarly, here's a sample that verifies a timeout/1 callback always returns
some number of milliseconds:
test "allowing a multiple of the attempt as job timeout" do
assert 1000 == MyWorker.timeout(%Oban.Job{attempt: 1})
assert 2000 == MyWorker.timeout(%Oban.Job{attempt: 2})
end
Jobs are Ecto schemas, and therefore structs. There isn't anything magical about
them! Explore the Oban.Job documentation to see all of the types and fields
available for testing.