delayed 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +20 -0
  3. data/README.md +560 -0
  4. data/Rakefile +35 -0
  5. data/lib/delayed.rb +72 -0
  6. data/lib/delayed/active_job_adapter.rb +65 -0
  7. data/lib/delayed/backend/base.rb +166 -0
  8. data/lib/delayed/backend/job_preparer.rb +43 -0
  9. data/lib/delayed/exceptions.rb +14 -0
  10. data/lib/delayed/job.rb +250 -0
  11. data/lib/delayed/lifecycle.rb +85 -0
  12. data/lib/delayed/message_sending.rb +65 -0
  13. data/lib/delayed/monitor.rb +134 -0
  14. data/lib/delayed/performable_mailer.rb +22 -0
  15. data/lib/delayed/performable_method.rb +47 -0
  16. data/lib/delayed/plugin.rb +15 -0
  17. data/lib/delayed/plugins/connection.rb +13 -0
  18. data/lib/delayed/plugins/instrumentation.rb +39 -0
  19. data/lib/delayed/priority.rb +164 -0
  20. data/lib/delayed/psych_ext.rb +135 -0
  21. data/lib/delayed/railtie.rb +7 -0
  22. data/lib/delayed/runnable.rb +46 -0
  23. data/lib/delayed/serialization/active_record.rb +18 -0
  24. data/lib/delayed/syck_ext.rb +42 -0
  25. data/lib/delayed/tasks.rb +40 -0
  26. data/lib/delayed/worker.rb +233 -0
  27. data/lib/delayed/yaml_ext.rb +10 -0
  28. data/lib/delayed_job.rb +1 -0
  29. data/lib/delayed_job_active_record.rb +1 -0
  30. data/lib/generators/delayed/generator.rb +7 -0
  31. data/lib/generators/delayed/migration_generator.rb +28 -0
  32. data/lib/generators/delayed/next_migration_version.rb +14 -0
  33. data/lib/generators/delayed/templates/migration.rb +22 -0
  34. data/spec/autoloaded/clazz.rb +6 -0
  35. data/spec/autoloaded/instance_clazz.rb +5 -0
  36. data/spec/autoloaded/instance_struct.rb +6 -0
  37. data/spec/autoloaded/struct.rb +7 -0
  38. data/spec/database.yml +25 -0
  39. data/spec/delayed/active_job_adapter_spec.rb +267 -0
  40. data/spec/delayed/job_spec.rb +953 -0
  41. data/spec/delayed/monitor_spec.rb +276 -0
  42. data/spec/delayed/plugins/instrumentation_spec.rb +49 -0
  43. data/spec/delayed/priority_spec.rb +154 -0
  44. data/spec/delayed/serialization/active_record_spec.rb +15 -0
  45. data/spec/delayed/tasks_spec.rb +116 -0
  46. data/spec/helper.rb +196 -0
  47. data/spec/lifecycle_spec.rb +77 -0
  48. data/spec/message_sending_spec.rb +149 -0
  49. data/spec/performable_mailer_spec.rb +68 -0
  50. data/spec/performable_method_spec.rb +123 -0
  51. data/spec/psych_ext_spec.rb +94 -0
  52. data/spec/sample_jobs.rb +117 -0
  53. data/spec/worker_spec.rb +235 -0
  54. data/spec/yaml_ext_spec.rb +48 -0
  55. metadata +326 -0
@@ -0,0 +1,15 @@
1
+ require "helper"
2
+
3
+ describe ActiveRecord do
4
+ it "loads classes with non-default primary key" do
5
+ expect {
6
+ YAML.load(Story.create.to_yaml)
7
+ }.not_to raise_error
8
+ end
9
+
10
+ it "loads classes even if not in default scope" do
11
+ expect {
12
+ YAML.load(Story.create(scoped: false).to_yaml)
13
+ }.not_to raise_error
14
+ end
15
+ end
@@ -0,0 +1,116 @@
1
+ require 'helper'
2
+
3
+ describe 'rake' do
4
+ let(:runnable) { instance_double(Delayed::Runnable, start: true) }
5
+
6
+ before do
7
+ Rake::Task.clear
8
+ Rake::Task.define_task(:environment)
9
+ load 'lib/delayed/tasks.rb'
10
+
11
+ allow(ENV).to receive(:[]).and_call_original
12
+ allow(ENV).to receive(:key?).and_call_original
13
+ end
14
+
15
+ def stub_env(key, value)
16
+ allow(ENV).to receive(:[]).with(key).and_return(value)
17
+ allow(ENV).to receive(:key?).with(key).and_return(true)
18
+ end
19
+
20
+ describe 'delayed:work' do
21
+ before do
22
+ allow(Delayed::Worker).to receive(:new).and_return(runnable)
23
+ end
24
+
25
+ it 'starts a worker' do
26
+ Rake.application.invoke_task 'delayed:work'
27
+ expect(runnable).to have_received(:start)
28
+ end
29
+
30
+ context 'when configurations are set manually' do
31
+ before do
32
+ Delayed::Worker.min_priority = 99
33
+ Delayed::Worker.max_priority = 101
34
+ Delayed::Worker.queues = %w(a b c)
35
+ Delayed::Worker.sleep_delay = 1.5
36
+ Delayed::Worker.read_ahead = 100
37
+ Delayed::Worker.max_claims = 10
38
+ end
39
+
40
+ it 'does not override them' do
41
+ Rake.application.invoke_task('delayed:work')
42
+
43
+ expect(Delayed::Worker.min_priority).to eq 99
44
+ expect(Delayed::Worker.max_priority).to eq 101
45
+ expect(Delayed::Worker.queues).to eq %w(a b c)
46
+ expect(Delayed::Worker.sleep_delay).to eq 1.5
47
+ expect(Delayed::Worker.read_ahead).to eq 100
48
+ expect(Delayed::Worker.max_claims).to eq 10
49
+ end
50
+ end
51
+
52
+ context 'when environment variables are set' do
53
+ before do
54
+ stub_env('MIN_PRIORITY', '6')
55
+ stub_env('MAX_PRIORITY', '8')
56
+ stub_env('QUEUES', 'foo,bar')
57
+ stub_env('SLEEP_DELAY', '1')
58
+ stub_env('READ_AHEAD', '3')
59
+ stub_env('MAX_CLAIMS', '3')
60
+ end
61
+
62
+ it 'sets the worker config' do
63
+ expect { Rake.application.invoke_task('delayed:work') }
64
+ .to change { Delayed::Worker.min_priority }.from(nil).to(6)
65
+ .and change { Delayed::Worker.max_priority }.from(nil).to(8)
66
+ .and change { Delayed::Worker.queues }.from([]).to(%w(foo bar))
67
+ .and change { Delayed::Worker.sleep_delay }.from(5).to(1)
68
+ .and change { Delayed::Worker.read_ahead }.from(5).to(3)
69
+ .and change { Delayed::Worker.max_claims }.from(5).to(3)
70
+ end
71
+ end
72
+ end
73
+
74
+ describe 'delayed:monitor' do
75
+ before do
76
+ allow(Delayed::Monitor).to receive(:new).and_return(runnable)
77
+ end
78
+
79
+ it 'starts a monitor' do
80
+ Rake.application.invoke_task 'delayed:monitor'
81
+ expect(runnable).to have_received(:start)
82
+ end
83
+
84
+ context 'when environment variables are set' do
85
+ before do
86
+ stub_env('MIN_PRIORITY', '6')
87
+ stub_env('MAX_PRIORITY', '8')
88
+ stub_env('QUEUE', 'foo')
89
+ stub_env('SLEEP_DELAY', '1')
90
+ stub_env('READ_AHEAD', '3')
91
+ stub_env('MAX_CLAIMS', '3')
92
+ end
93
+
94
+ it 'sets the worker config' do
95
+ expect { Rake.application.invoke_task('delayed:monitor') }
96
+ .to change { Delayed::Worker.min_priority }.from(nil).to(6)
97
+ .and change { Delayed::Worker.max_priority }.from(nil).to(8)
98
+ .and change { Delayed::Worker.queues }.from([]).to(%w(foo))
99
+ .and change { Delayed::Worker.sleep_delay }.from(5).to(1)
100
+ .and change { Delayed::Worker.read_ahead }.from(5).to(3)
101
+ .and change { Delayed::Worker.max_claims }.from(5).to(3)
102
+ end
103
+ end
104
+ end
105
+
106
+ describe 'jobs:work' do
107
+ before do
108
+ allow(Delayed::Worker).to receive(:new).and_return(runnable)
109
+ end
110
+
111
+ it 'starts a worker' do
112
+ Rake.application.invoke_task 'jobs:work'
113
+ expect(runnable).to have_received(:start)
114
+ end
115
+ end
116
+ end
data/spec/helper.rb ADDED
@@ -0,0 +1,196 @@
1
+ require 'rspec'
2
+ require 'timecop'
3
+
4
+ require 'action_mailer'
5
+ require 'active_job'
6
+ require 'active_record'
7
+
8
+ require 'delayed'
9
+ require 'sample_jobs'
10
+
11
+ require 'rake'
12
+
13
+ if ENV['DEBUG_LOGS']
14
+ Delayed.logger = Logger.new($stdout)
15
+ else
16
+ require 'tempfile'
17
+
18
+ tf = Tempfile.new('dj.log')
19
+ Delayed.logger = Logger.new(tf.path)
20
+ tf.unlink
21
+ end
22
+ ENV['RAILS_ENV'] = 'test'
23
+
24
+ db_adapter = ENV["ADAPTER"]
25
+ gemfile = ENV["BUNDLE_GEMFILE"]
26
+ db_adapter ||= gemfile && gemfile[%r{gemfiles/(.*?)/}] && $1 # rubocop:disable Style/PerlBackrefs
27
+ db_adapter ||= "sqlite3"
28
+
29
+ config = YAML.load(ERB.new(File.read("spec/database.yml")).result)
30
+ ActiveRecord::Base.establish_connection config[db_adapter]
31
+ ActiveRecord::Base.logger = Delayed.logger
32
+ ActiveRecord::Migration.verbose = false
33
+
34
+ # MySQL 5.7 no longer supports null default values for the primary key
35
+ # Override the default primary key type in Rails <= 4.0
36
+ # https://stackoverflow.com/a/34555109
37
+ if db_adapter == "mysql2"
38
+ types = if defined?(ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter)
39
+ # ActiveRecord 3.2+
40
+ ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::NATIVE_DATABASE_TYPES
41
+ else
42
+ # ActiveRecord < 3.2
43
+ ActiveRecord::ConnectionAdapters::Mysql2Adapter::NATIVE_DATABASE_TYPES
44
+ end
45
+ types[:primary_key] = types[:primary_key].sub(" DEFAULT NULL", "")
46
+ end
47
+
48
+ migration_template = File.open("lib/generators/delayed/templates/migration.rb")
49
+
50
+ # need to eval the template with the migration_version intact
51
+ migration_context =
52
+ Class.new do
53
+ def my_binding
54
+ binding
55
+ end
56
+
57
+ private
58
+
59
+ def migration_version
60
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]" if ActiveRecord::VERSION::MAJOR >= 5
61
+ end
62
+ end
63
+
64
+ migration_ruby = ERB.new(migration_template.read).result(migration_context.new.my_binding)
65
+ eval(migration_ruby) # rubocop:disable Security/Eval
66
+
67
+ ActiveRecord::Schema.define do
68
+ if table_exists?(:delayed_jobs)
69
+ # `if_exists: true` was only added in Rails 5
70
+ drop_table :delayed_jobs
71
+ end
72
+
73
+ CreateDelayedJobs.up
74
+
75
+ create_table :stories, primary_key: :story_id, force: true do |table|
76
+ table.string :text
77
+ table.boolean :scoped, default: true
78
+ end
79
+ end
80
+
81
+ class Story < ActiveRecord::Base
82
+ self.primary_key = :story_id
83
+
84
+ def tell
85
+ text
86
+ end
87
+
88
+ def whatever(number)
89
+ tell * number
90
+ end
91
+ default_scope { where(scoped: true) }
92
+
93
+ handle_asynchronously :whatever
94
+ end
95
+
96
+ class SingletonClass
97
+ include Singleton
98
+ end
99
+
100
+ RSpec.configure do |config|
101
+ config.around(:each) do |example|
102
+ aj_priority_was = ActiveJob::Base.priority
103
+ aj_queue_name_was = ActiveJob::Base.queue_name
104
+ default_priority_was = Delayed::Worker.default_priority
105
+ default_queue_name_was = Delayed::Worker.default_queue_name
106
+ delay_jobs_was = Delayed::Worker.delay_jobs
107
+ destroy_failed_jobs_was = Delayed::Worker.destroy_failed_jobs
108
+ max_attempts_was = Delayed::Worker.max_attempts
109
+ max_claims_was = Delayed::Worker.max_claims
110
+ max_priority_was = Delayed::Worker.max_priority
111
+ max_run_time_was = Delayed::Worker.max_run_time
112
+ min_priority_was = Delayed::Worker.min_priority
113
+ queues_was = Delayed::Worker.queues
114
+ read_ahead_was = Delayed::Worker.read_ahead
115
+ sleep_delay_was = Delayed::Worker.sleep_delay
116
+
117
+ example.run
118
+ ensure
119
+ ActiveJob::Base.priority = aj_priority_was
120
+ ActiveJob::Base.queue_name = aj_queue_name_was
121
+ Delayed::Worker.default_priority = default_priority_was
122
+ Delayed::Worker.default_queue_name = default_queue_name_was
123
+ Delayed::Worker.delay_jobs = delay_jobs_was
124
+ Delayed::Worker.destroy_failed_jobs = destroy_failed_jobs_was
125
+ Delayed::Worker.max_attempts = max_attempts_was
126
+ Delayed::Worker.max_claims = max_claims_was
127
+ Delayed::Worker.max_priority = max_priority_was
128
+ Delayed::Worker.max_run_time = max_run_time_was
129
+ Delayed::Worker.min_priority = min_priority_was
130
+ Delayed::Worker.queues = queues_was
131
+ Delayed::Worker.read_ahead = read_ahead_was
132
+ Delayed::Worker.sleep_delay = sleep_delay_was
133
+
134
+ Delayed::Job.delete_all
135
+ end
136
+
137
+ config.expect_with :rspec do |c|
138
+ c.syntax = :expect
139
+ end
140
+ end
141
+
142
+ # Add this directory so the ActiveSupport autoloading works
143
+ ActiveSupport::Dependencies.autoload_paths << File.dirname(__FILE__)
144
+
145
+ RSpec::Matchers.define :emit_notification do |expected_event_name|
146
+ attr_reader :actual, :expected
147
+
148
+ def supports_block_expectations?
149
+ true
150
+ end
151
+
152
+ chain :with_payload, :expected_payload
153
+ chain :with_value, :expected_value
154
+ diffable
155
+
156
+ match do |block|
157
+ @expected = { event_name: expected_event_name, payload: expected_payload, value: expected_value }
158
+ @actuals = []
159
+ callback = ->(name, _started, _finished, _unique_id, payload) do
160
+ @actuals << { event_name: name, payload: payload.except(:value), value: payload[:value] }
161
+ end
162
+
163
+ ActiveSupport::Notifications.subscribed(callback, expected_event_name, &block)
164
+
165
+ unless expected_payload
166
+ @actuals.each { |a| a.delete(:payload) }
167
+ @expected.delete(:payload)
168
+ end
169
+
170
+ @actual = @actuals.select { |a| values_match?(@expected.except(:value), a.except(:value)) }
171
+ @expected = [@expected]
172
+ values_match?(@expected, @actual)
173
+ end
174
+
175
+ failure_message do
176
+ <<~MSG
177
+ Expected the code block to emit:
178
+ #{@expected.first.inspect}
179
+
180
+ But instead, the following were emitted:
181
+ #{(@actual.presence || @actuals).map(&:inspect).join("\n ")}
182
+ MSG
183
+ end
184
+ end
185
+
186
+ def current_adapter
187
+ ENV.fetch('ADAPTER', 'sqlite3')
188
+ end
189
+
190
+ def current_database
191
+ if current_adapter == 'sqlite3'
192
+ a_string_ending_with('tmp/database.sqlite')
193
+ else
194
+ 'delayed_job_test'
195
+ end
196
+ end
@@ -0,0 +1,77 @@
1
+ require 'helper'
2
+
3
+ describe Delayed::Lifecycle do
4
+ let(:lifecycle) { described_class.new }
5
+ let(:callback) { lambda { |*_args| } }
6
+ let(:arguments) { [1] }
7
+ let(:behavior) { double(Object, before!: nil, after!: nil, inside!: nil) }
8
+ let(:wrapped_block) { proc { behavior.inside! } }
9
+
10
+ describe 'before callbacks' do
11
+ before(:each) do
12
+ lifecycle.before(:enqueue, &callback)
13
+ end
14
+
15
+ it 'enqueues before wrapped block' do
16
+ expect(callback).to receive(:call).with(*arguments).ordered
17
+ expect(behavior).to receive(:inside!).ordered
18
+ lifecycle.run_callbacks :enqueue, *arguments, &wrapped_block
19
+ end
20
+ end
21
+
22
+ describe 'after callbacks' do
23
+ before(:each) do
24
+ lifecycle.after(:enqueue, &callback)
25
+ end
26
+
27
+ it 'enqueues after wrapped block' do
28
+ expect(behavior).to receive(:inside!).ordered
29
+ expect(callback).to receive(:call).with(*arguments).ordered
30
+ lifecycle.run_callbacks :enqueue, *arguments, &wrapped_block
31
+ end
32
+ end
33
+
34
+ describe 'around callbacks' do
35
+ before(:each) do
36
+ lifecycle.around(:enqueue) do |*args, &block|
37
+ behavior.before!
38
+ block.call(*args)
39
+ behavior.after!
40
+ end
41
+ end
42
+
43
+ it 'wraps a block' do
44
+ expect(behavior).to receive(:before!).ordered
45
+ expect(behavior).to receive(:inside!).ordered
46
+ expect(behavior).to receive(:after!).ordered
47
+ lifecycle.run_callbacks :enqueue, *arguments, &wrapped_block
48
+ end
49
+
50
+ it 'enqueues multiple callbacks in order' do
51
+ expect(behavior).to receive(:one).ordered
52
+ expect(behavior).to receive(:two).ordered
53
+ expect(behavior).to receive(:three).ordered
54
+
55
+ lifecycle.around(:enqueue) do |*args, &block|
56
+ behavior.one
57
+ block.call(*args)
58
+ end
59
+ lifecycle.around(:enqueue) do |*args, &block|
60
+ behavior.two
61
+ block.call(*args)
62
+ end
63
+ lifecycle.around(:enqueue) do |*args, &block|
64
+ behavior.three
65
+ block.call(*args)
66
+ end
67
+ lifecycle.run_callbacks(:enqueue, *arguments, &wrapped_block)
68
+ end
69
+ end
70
+
71
+ it 'raises if callback is enqueued with wrong number of parameters' do
72
+ lifecycle.before(:enqueue, &callback)
73
+ expect {
74
+ lifecycle.run_callbacks(:enqueue, 1, 2, 3) {} # no-op
75
+ }.to raise_error(ArgumentError, /1 parameter/)
76
+ end
77
+ end
@@ -0,0 +1,149 @@
1
+ require 'helper'
2
+
3
+ describe Delayed::MessageSending do
4
+ it 'does not include ClassMethods along with MessageSending' do
5
+ expect { ClassMethods }.to raise_error(NameError)
6
+ expect(defined?(String::ClassMethods)).to eq(nil)
7
+ end
8
+
9
+ describe 'handle_asynchronously' do
10
+ class Story
11
+ def tell!(_arg); end
12
+ handle_asynchronously :tell!
13
+ end
14
+
15
+ it 'aliases original method' do
16
+ expect(Story.new).to respond_to(:tell_without_delay!)
17
+ expect(Story.new).to respond_to(:tell_with_delay!)
18
+ end
19
+
20
+ it 'creates a PerformableMethod' do
21
+ story = Story.create
22
+ expect {
23
+ job = story.tell!(1)
24
+ expect(job.payload_object.class).to eq(Delayed::PerformableMethod)
25
+ expect(job.payload_object.method_name).to eq(:tell_without_delay!)
26
+ expect(job.payload_object.args).to eq([1])
27
+ }.to(change { Delayed::Job.count })
28
+ end
29
+
30
+ describe 'with options' do
31
+ class Fable
32
+ cattr_accessor :importance
33
+ def tell; end
34
+ handle_asynchronously :tell, priority: proc { importance }
35
+ end
36
+
37
+ it 'sets the priority based on the Fable importance' do
38
+ Fable.importance = 10
39
+ job = Fable.new.tell
40
+ expect(job.priority).to eq(10)
41
+
42
+ Fable.importance = 20
43
+ job = Fable.new.tell
44
+ expect(job.priority).to eq(20)
45
+ end
46
+
47
+ describe 'using a proc with parameters' do
48
+ class Yarn
49
+ attr_accessor :importance
50
+
51
+ def spin; end
52
+ handle_asynchronously :spin, priority: proc { |y| y.importance }
53
+ end
54
+
55
+ it 'sets the priority based on the Fable importance' do
56
+ job = Yarn.new.tap { |y| y.importance = 10 }.spin
57
+ expect(job.priority).to eq(10)
58
+
59
+ job = Yarn.new.tap { |y| y.importance = 20 }.spin
60
+ expect(job.priority).to eq(20)
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ context 'delay' do
67
+ class FairyTail
68
+ attr_accessor :happy_ending
69
+
70
+ def self.princesses; end
71
+
72
+ def tell
73
+ @happy_ending = true
74
+ end
75
+ end
76
+
77
+ after do
78
+ Delayed::Worker.default_queue_name = nil
79
+ end
80
+
81
+ it 'creates a new PerformableMethod job' do
82
+ expect {
83
+ job = 'hello'.delay.count('l')
84
+ expect(job.payload_object.class).to eq(Delayed::PerformableMethod)
85
+ expect(job.payload_object.method_name).to eq(:count)
86
+ expect(job.payload_object.args).to eq(['l'])
87
+ }.to change { Delayed::Job.count }.by(1)
88
+ end
89
+
90
+ it 'sets default priority' do
91
+ Delayed::Worker.default_priority = 99
92
+ job = FairyTail.delay.to_s
93
+ expect(job.priority).to eq(99)
94
+ end
95
+
96
+ it 'sets default queue name' do
97
+ Delayed::Worker.default_queue_name = 'abbazabba'
98
+ job = FairyTail.delay.to_s
99
+ expect(job.queue).to eq('abbazabba')
100
+ end
101
+
102
+ it 'sets job options' do
103
+ run_at = Time.parse('2010-05-03 12:55 AM')
104
+ job = FairyTail.delay(priority: 20, run_at: run_at).to_s
105
+ expect(job.run_at).to eq(run_at)
106
+ expect(job.priority).to eq(20)
107
+ end
108
+
109
+ it 'does not delay the job when delay_jobs is false' do
110
+ Delayed::Worker.delay_jobs = false
111
+ fairy_tail = FairyTail.new
112
+ expect {
113
+ expect {
114
+ fairy_tail.delay.tell
115
+ }.to change { fairy_tail.happy_ending }.from(nil).to(true)
116
+ }.not_to(change { Delayed::Job.count })
117
+ end
118
+
119
+ it 'does delay the job when delay_jobs is true' do
120
+ Delayed::Worker.delay_jobs = true
121
+ fairy_tail = FairyTail.new
122
+ expect {
123
+ expect {
124
+ fairy_tail.delay.tell
125
+ }.not_to change { fairy_tail.happy_ending }
126
+ }.to change { Delayed::Job.count }.by(1)
127
+ end
128
+
129
+ it 'does delay when delay_jobs is a proc returning true' do
130
+ Delayed::Worker.delay_jobs = ->(_job) { true }
131
+ fairy_tail = FairyTail.new
132
+ expect {
133
+ expect {
134
+ fairy_tail.delay.tell
135
+ }.not_to change { fairy_tail.happy_ending }
136
+ }.to change { Delayed::Job.count }.by(1)
137
+ end
138
+
139
+ it 'does not delay the job when delay_jobs is a proc returning false' do
140
+ Delayed::Worker.delay_jobs = ->(_job) { false }
141
+ fairy_tail = FairyTail.new
142
+ expect {
143
+ expect {
144
+ fairy_tail.delay.tell
145
+ }.to change { fairy_tail.happy_ending }.from(nil).to(true)
146
+ }.not_to(change { Delayed::Job.count })
147
+ end
148
+ end
149
+ end