-
Notifications
You must be signed in to change notification settings - Fork 30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
What is the recommended way to run TaskBunny in test env? #44
Comments
Good question. At Square Enix, we don't enqueue jobs to RabbitMQ when we run tests for our applications. We wrote a little test helper to mock enqueue in our application. defmodule YourApp.TaskBunnyHelper do
alias TaskBunny.{Publisher, Queue, Message}
@doc """
Mocks TaskBunny.Queue.Publisher so that we can isolate our tests from RabbitMQ.
"""
def mock_publish do
:meck.expect Publisher, :publish!, fn (_host, _queue, _message) ->
:ok
end
:meck.expect Publisher, :publish!, fn (_host, _queue, _message, _options) ->
:ok
end
:meck.expect Queue, :declare_with_subqueues, fn (_host, _queue) ->
:ok
end
end
@doc """
Mocks TaskBunny.Publisher and performs jobs given immediately.
Once you call `TaskBunnyHelper.sync_publish()`, `TaskBunny.Publisher` will perform the job immediately instead of sending a message to queue.
"""
def sync_publish do
:meck.expect Publisher, :publish!, fn (_host, _queue, message, _option) ->
{:ok, json} = Message.decode(message)
json["job"].perform(json["payload"])
end
end
@doc """
Check if the job is enqueued with given condition
"""
def enqueued?(job, payload \\ nil) do
history = :meck.history(Publisher)
queued = Enum.find history, fn ({_pid, {_module, :publish!, args}, _ret}) ->
case args do
[_h, _q, message | _] ->
{:ok, json} = Message.decode(message)
json["job"] == job && (is_nil(payload) || json["payload"] == payload)
_ -> false
end
end
queued != nil
end
end On setup you call the function. TaskBunnyHelper.mock_publish()
on_exit(&:meck.unload/0) It also helps you to test if the job was enqueued with the expected parameters too. assert TaskBunnyHelper.enqueued?(MyJob, %{"name" => "Loto"}) Two things you might want to be aware if you want to use this snippet.
We don't provide it as an official way since we can imagine people have different preference on test approach but we are always happy to discuss the idea and share what we do here. Hope the information above helps your situation. Thanks! |
@ono thanks for sharing that example - this evening I watched your ElixirConf presentation from earlier this year to see if you mentioned the approach you take, unfortunately you didn't but I enjoyed the talk anyway. I was thinking of doing something fairly similar to the code you posted except I was going to take the approach that thoughtbot/bamboo did and configure it through an adapter. The main reason I was thinking that would be a better solution than Thanks again. |
@ono We've been looking at this as well. It seems the community has been learning towards explicit contracts as a way to establish mocks at the boundary of a given module. By defining a behaviour for |
Here is where we ended up. This may be a bit heavy handed, but it has allowed us to mock out our job execution using the Mox lib. First, we created a stub module that duplicates the functionality of Stub Module defmodule TaskBunny.Job.Stub do
@moduledoc """
Provides a test harness for mocking enqueue calls to TaskBunny jobs.
"""
alias TaskBunny.Job.Mock
alias TaskBunny.Job.QueueNotFoundError
alias TaskBunny.Connection.ConnectError
alias TaskBunny.Publisher.PublishError
@callback enqueue(atom, any, keyword) :: :ok | {:error, any}
@callback enqueue!(atom, any, keyword) :: :ok
defmacro __using__(_options \\ []) do
quote do
@behaviour TaskBunny.Job
def enqueue(payload, options \\ []) do
Mock.enqueue(__MODULE__, payload, options)
rescue
e in [ConnectError, PublishError, QueueNotFoundError] -> {:error, e}
end
def enqueue!(payload, options \\ []), do: Mock.enqueue!(__MODULE__, payload, options)
def timeout, do: 120_000
def max_retry, do: 10
def retry_interval(_failed_count), do: 300_000
defoverridable [timeout: 0, max_retry: 0, retry_interval: 1]
end
end
end This stub is swapped in for the test config. config :task_bunny, :job_module, TaskBunny.Job.Stub def job do
quote do
use unquote(Application.get_env(:task_bunny, :job_module, TaskBunny.Job))
end
end We establish the mock with Mox: Mox.defmock(TaskBunny.Job.Mock, for: TaskBunny.Job.Stub) And finally in the test... test "enqueues for known job" do
TaskBunny.Job.Mock
|> expect(:enqueue, fn (FooJob, %{"target" => "a"}, _) -> :ok end)
|> expect(:enqueue, fn (FooJob, %{"target" => "b"}, _) -> :ok end)
integrations = ~w(a b)
message = %{"integrations" => integrations, "job" => to_string(FooJob), "payload" => %{"foo" => "bar"}}
assert FanoutJob.perform(message) == :ok
end Profit!!!
|
Just thought I would post another alternative. With my projects, I have been using my GenQueue abstraction to work with different forms of background job queues. Typically, we might have a "simple" queue that uses something like GenStage, and a more "durable" queue with something like TaskBunny. With adapters, GenQueue lets me use a common API for both - as well as a unified testing format using assert_recieve. With TaskBunny, one can just drop in the TaskBunny adapter. |
At Square Enix how do you run TaskBunny in test env?
From the documentation I can see that it is possible to disable workers (which I do) but it doesn't seem possible to swap the backend to process inline or even not enqueue a job. As a concrete example, for the following code I want to write some tests:
Tests:
do_something_with_args/1
NOTE: job enqueuing usually happens in the web process and job working happens in a separate app (umbrella).
At the moment I don't have a good solution for
MyModule.call/1
that includes both [1] and [2] - I've been writing a test for [1] and a separate unit test specifically forMyJob
to handle [2]. Also, because I disable workers in tests my queues are full of jobs that try and get worked when I boot the app.It is likely that I am approaching the problem wrong because I am used to rails' active_job which provides an abstraction around job processing libraries which means in test env I can swap out the backend for a "same process, immediate worker" backend. I'm fairly certain that the goal of TaskBunny isn't to provide that functionality (makes total sense) but I'd like to know how you've approached solving these problems.
The text was updated successfully, but these errors were encountered: