🕒 Scheduled jobs invocation emulation for test environments (eliminate time traveling by might and magic 😈)
MIT License
hackaton slides: link
meetup slides: link
meetup video: link
Sidekiq::Portal - scheduled jobs runner for your test environments,
which execution process must occur during the Timecop.travel(...)
operations according to the scheduler config.
Each job starts at the time it was supposed to start according to the scheduler plan -
the internal Time.current
expression will give you exactly the scheduler-planned time.
Supports ActiveJob backend (Sidekiq::Worker coming soon). Works with sidekiq-scheduler-based job configs (sidekiq-cron coming soon).
Realized as an instance, but have a global-based implementation too.
Gemfile
gem 'sidekiq', '>= 5' # runtime dependency
gem 'timecop', '~> 0.9' # runtime dependency
group :test do
gem 'rspec'
gem 'sidekiq_portal'
end
bundle install
spec_helper.rb
:require 'timecop' # runtime dependency
require 'sidekiq' # runtime dependency
require 'sidekiq/api'
require 'sidekiq/testing'
require 'sidekiq_portal'
default_timezone
- global time zone for your jobs (UTC
by default);retry_count
- Sidekiq's built-in retry mechanism simulation (0
by default);retry_on
- retry only on a set of exceptions ([StandardError]
by default);scheduler_config
- sidekiq-scheduler
-based scheduler configuration ({}
by default (non-configured));Sidekiq::Portal.reload!(&configuration)
- reload portal configurations;In your spec_helper.rb
:
# portal configuration
Sidekiq::Portal.setup! do |config|
config.default_timezone = 'UTC' # 'UTC' by default
config.retry_count = 3 # 0 by default
config.retry_on = [StandardError] # [StandardError] by default
# pre-defined sidekiq-scheduler configs (Rails example)
config.scheduler_config = Rails.application.config_for(:sidekiq)[:schedule]
# manual sidekiq-scheduler configs
config.scheduler_config = {
LoolJob: { every: '15m' },
kek_job: { cron: '0 * * * * *', class: :KekJob }
}
end
# global state clearing logic
RSpec.configure do |config|
config.before { Sidekiq::Worker.clear_all }
config.after { Timecop.return }
config.after { Sidekiq::Portal.reload! }
end
And in your tests:
RSpec.describe 'Some spec' do
specify 'magic?' do
Timecop.travel(Time.current + 2.hours) # magic begins here 😈
end
end
class HookExampleJob < ApplicationJob
def perform
GLOBAL_HOOK_INTERCEPTOR << Time.current # intercept current time
end
end
Sidekiq::Scheduler
config::schedule:
HookExample:
every: '15m'
HookExampleJob
spec:RSpec.describe 'HookExampleJob sheduler plan' do
specify 'scheduled?' do
stub_const('GLOBAL_HOOK_INTERCEPTOR', [])
expect(GLOBAL_HOOK_INTERCEPTOR.count).to eq(0) # => true
# do some magic 😈
Timecop.travel(Time.current + 2.hours)
expect(GLOBAL_HOOK_INTERCEPTOR.count).to eq(8) # => true (😈 magic)
puts GLOBAL_HOOK_INTERCEPTOR # 😈
# => outputs:
# 2019-12-24 03:05:39 +0300 (+15m) (Time.current from HookExampleJob#perform)
# 2019-12-24 03:20:39 +0300 (+15m)
# 2019-12-24 03:35:39 +0300 (+15m)
# 2019-12-24 03:50:39 +0300 (+15m)
# 2019-12-24 04:05:39 +0300 (+15m)
# 2019-12-24 04:20:39 +0300 (+15m)
# 2019-12-24 04:35:39 +0300 (+15m)
# 2019-12-24 04:50:39 +0300 (+15m)
end
end
Sidekiq::Testing.portal!
test mode with support for :inline
and :fake
;Sidekiq::Testing.inline!
and Sidekiq::Testing.fake
respectively);ActiveSupport::TimeZone
instances in default_timezone
config;#reload!
should use previously defined settings?;Sidekiq::Worker
job backend;Sidekiq::Cron
scheduler plans;Sidekiq::Portal.new(&configuration)
);Released under MIT License.