Run DelayedJob Manually in Test Env
# in config/initializers/delayed_job_config.rb Delayed::Worker.delay_jobs = true # in your spec Delayed::Worker.new.work_off
Common Setup of DelayedJob
Assume you follow DelayedJob readme example to configure it like this:
Delayed::Worker.delay_jobs = !Rails.env.test?, what it does is in test env it doesn’t delay the job, meaning DelayedJob is being transparent, the job you put will be executed in “real time”. In most cases you don’t even need to worry about it, your test should be just fine, but recently it caught me…
It Fails When…
To give some background, I’m working on a API centric rails project. In order to authenticate with API we pass in access token for every request, and that’s done in the middleware layer. Since access token is stored in cookie, and in middleware we can’t access browser cookie directly, so another tool called RequestStore is used. If in the same request, what you stored in RequestStore you can access it later no matter the context, a unrealistic example would be you store a cookie value to RequestStore then use it in model later. Don’t do that :).
The code below is a simplified version to illustrate the flow.
class ApplicationController < ActionController::Base before_action :set_api_access_token def set_api_access_token RequestStore.store[:access_token] = cookies.signed[:access_token] end end class Authentication < Faraday::Middleware def call(env) env[:request_headers]['Authorization'] = RequestStore[:access_token] if RequestStore[:access_token] @app.call(env) end end
Every api request happened inside the rails
ApplicationController stack should have the access token being set, but what would happen in a different context like rake task or DelayedJob where you need to send request to the API? The
before_action is not gonna be executed there so
RequestStore[:access_token] would be nil. This is an easy-to-spot issue if you try it once, but if you follow the TDD work flow and write test for it first, then it’ll fail you.
Delayed::Worker.delay_jobs set to
false in test env, the job will be executed immediately in the same request, so the
RequestStore[:access_token] still contains the value and will pass to the Authorization header in the middleware, spec passed but but in real world env it failed. Typical false positive result.
To Run It Manually
# in config/initializers/delayed_job_config.rb Delayed::Worker.delay_jobs = true # in your spec # here is the code to enqueue a job to DelayedJob queue visit post_path(post) # run it manually Delayed::Worker.new.work_off # expectation expect(api_endpoint).to have_been_requested end
Delayed::Worker.new.work_off returns an Array like
[1, 0] indicating succeeded job counts and failed job counts. I’ve also seen some people testing against that like
expect(Delayed::Worker.new.work_off).to eq([1, 0]), personally I don’t think it’s necessary.
- You have your own expectation right after that and that should be the main concern of the spec. If the job failed, your spec should be failed too.
- What if multiple jobs are enqueued while you’re only focusing one of them in the spec? Update the value to
[2, 0]? That’s just noise.
I guess what I encountered is a rare case, but definitely an interesting case. I kinda prefer this way to mimic real world environment to prevent any possible regressions.
Commercial time: If you’re about to build a API centric rails app, be sure to check out the awesome gem called spyke made by @balvig, the slogan is “Interact with remote REST services in an ActiveRecord-like manner.”