rspec-goodies 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 688595f20fe784a45182edf3ebdcb7f19a411ad3dd975259b0022631d925f788
4
+ data.tar.gz: f9a6bd4961893ceab3d91de8592d6ce8ca883d2c68a1706212144e0d74972dcd
5
+ SHA512:
6
+ metadata.gz: 430633d6935b63b479bc46e9fc1e02dcf65f73ddd5ab8aa38db6efe3adc4c0c85681c2ad0eccc00fd720eefaf958c0566b19e7ae7dc9f2132c223d940a33e5bc
7
+ data.tar.gz: 14841a867c03923ec9af50fa0eda9d02b5d88f9c64ba7dc9783ec4be283ab3fc4e865bdaf3c04d52dc11ee1202b23c6c8bf36277355944978915bceac8131191
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in rspec-goodies.gemspec
6
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,218 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ rspec-goodies (0.0.1)
5
+ activesupport
6
+ byebug
7
+ rails
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ actioncable (7.1.2)
13
+ actionpack (= 7.1.2)
14
+ activesupport (= 7.1.2)
15
+ nio4r (~> 2.0)
16
+ websocket-driver (>= 0.6.1)
17
+ zeitwerk (~> 2.6)
18
+ actionmailbox (7.1.2)
19
+ actionpack (= 7.1.2)
20
+ activejob (= 7.1.2)
21
+ activerecord (= 7.1.2)
22
+ activestorage (= 7.1.2)
23
+ activesupport (= 7.1.2)
24
+ mail (>= 2.7.1)
25
+ net-imap
26
+ net-pop
27
+ net-smtp
28
+ actionmailer (7.1.2)
29
+ actionpack (= 7.1.2)
30
+ actionview (= 7.1.2)
31
+ activejob (= 7.1.2)
32
+ activesupport (= 7.1.2)
33
+ mail (~> 2.5, >= 2.5.4)
34
+ net-imap
35
+ net-pop
36
+ net-smtp
37
+ rails-dom-testing (~> 2.2)
38
+ actionpack (7.1.2)
39
+ actionview (= 7.1.2)
40
+ activesupport (= 7.1.2)
41
+ nokogiri (>= 1.8.5)
42
+ racc
43
+ rack (>= 2.2.4)
44
+ rack-session (>= 1.0.1)
45
+ rack-test (>= 0.6.3)
46
+ rails-dom-testing (~> 2.2)
47
+ rails-html-sanitizer (~> 1.6)
48
+ actiontext (7.1.2)
49
+ actionpack (= 7.1.2)
50
+ activerecord (= 7.1.2)
51
+ activestorage (= 7.1.2)
52
+ activesupport (= 7.1.2)
53
+ globalid (>= 0.6.0)
54
+ nokogiri (>= 1.8.5)
55
+ actionview (7.1.2)
56
+ activesupport (= 7.1.2)
57
+ builder (~> 3.1)
58
+ erubi (~> 1.11)
59
+ rails-dom-testing (~> 2.2)
60
+ rails-html-sanitizer (~> 1.6)
61
+ activejob (7.1.2)
62
+ activesupport (= 7.1.2)
63
+ globalid (>= 0.3.6)
64
+ activemodel (7.1.2)
65
+ activesupport (= 7.1.2)
66
+ activerecord (7.1.2)
67
+ activemodel (= 7.1.2)
68
+ activesupport (= 7.1.2)
69
+ timeout (>= 0.4.0)
70
+ activestorage (7.1.2)
71
+ actionpack (= 7.1.2)
72
+ activejob (= 7.1.2)
73
+ activerecord (= 7.1.2)
74
+ activesupport (= 7.1.2)
75
+ marcel (~> 1.0)
76
+ activesupport (7.1.2)
77
+ base64
78
+ bigdecimal
79
+ concurrent-ruby (~> 1.0, >= 1.0.2)
80
+ connection_pool (>= 2.2.5)
81
+ drb
82
+ i18n (>= 1.6, < 2)
83
+ minitest (>= 5.1)
84
+ mutex_m
85
+ tzinfo (~> 2.0)
86
+ base64 (0.1.1)
87
+ bigdecimal (3.1.4)
88
+ builder (3.2.4)
89
+ byebug (11.1.3)
90
+ concurrent-ruby (1.2.2)
91
+ connection_pool (2.4.1)
92
+ crass (1.0.6)
93
+ date (3.3.4)
94
+ diff-lcs (1.5.0)
95
+ drb (2.1.1)
96
+ ruby2_keywords
97
+ erubi (1.12.0)
98
+ globalid (1.2.1)
99
+ activesupport (>= 6.1)
100
+ i18n (1.14.1)
101
+ concurrent-ruby (~> 1.0)
102
+ io-console (0.7.1)
103
+ irb (1.11.0)
104
+ rdoc
105
+ reline (>= 0.3.8)
106
+ loofah (2.22.0)
107
+ crass (~> 1.0.2)
108
+ nokogiri (>= 1.12.0)
109
+ mail (2.8.1)
110
+ mini_mime (>= 0.1.1)
111
+ net-imap
112
+ net-pop
113
+ net-smtp
114
+ marcel (1.0.2)
115
+ mini_mime (1.1.5)
116
+ minitest (5.20.0)
117
+ mutex_m (0.1.2)
118
+ net-imap (0.4.9)
119
+ date
120
+ net-protocol
121
+ net-pop (0.1.2)
122
+ net-protocol
123
+ net-protocol (0.2.2)
124
+ timeout
125
+ net-smtp (0.4.0)
126
+ net-protocol
127
+ nio4r (2.7.0)
128
+ nokogiri (1.15.5-arm64-darwin)
129
+ racc (~> 1.4)
130
+ psych (5.1.2)
131
+ stringio
132
+ racc (1.7.3)
133
+ rack (3.0.8)
134
+ rack-session (2.0.0)
135
+ rack (>= 3.0.0)
136
+ rack-test (2.1.0)
137
+ rack (>= 1.3)
138
+ rackup (2.1.0)
139
+ rack (>= 3)
140
+ webrick (~> 1.8)
141
+ rails (7.1.2)
142
+ actioncable (= 7.1.2)
143
+ actionmailbox (= 7.1.2)
144
+ actionmailer (= 7.1.2)
145
+ actionpack (= 7.1.2)
146
+ actiontext (= 7.1.2)
147
+ actionview (= 7.1.2)
148
+ activejob (= 7.1.2)
149
+ activemodel (= 7.1.2)
150
+ activerecord (= 7.1.2)
151
+ activestorage (= 7.1.2)
152
+ activesupport (= 7.1.2)
153
+ bundler (>= 1.15.0)
154
+ railties (= 7.1.2)
155
+ rails-dom-testing (2.2.0)
156
+ activesupport (>= 5.0.0)
157
+ minitest
158
+ nokogiri (>= 1.6)
159
+ rails-html-sanitizer (1.6.0)
160
+ loofah (~> 2.21)
161
+ nokogiri (~> 1.14)
162
+ railties (7.1.2)
163
+ actionpack (= 7.1.2)
164
+ activesupport (= 7.1.2)
165
+ irb
166
+ rackup (>= 1.0.0)
167
+ rake (>= 12.2)
168
+ thor (~> 1.0, >= 1.2.2)
169
+ zeitwerk (~> 2.6)
170
+ rake (13.1.0)
171
+ rdoc (6.6.2)
172
+ psych (>= 4.0.0)
173
+ redis-client (0.19.1)
174
+ connection_pool
175
+ reline (0.4.1)
176
+ io-console (~> 0.5)
177
+ rspec (3.12.0)
178
+ rspec-core (~> 3.12.0)
179
+ rspec-expectations (~> 3.12.0)
180
+ rspec-mocks (~> 3.12.0)
181
+ rspec-core (3.12.2)
182
+ rspec-support (~> 3.12.0)
183
+ rspec-expectations (3.12.3)
184
+ diff-lcs (>= 1.2.0, < 2.0)
185
+ rspec-support (~> 3.12.0)
186
+ rspec-mocks (3.12.6)
187
+ diff-lcs (>= 1.2.0, < 2.0)
188
+ rspec-support (~> 3.12.0)
189
+ rspec-support (3.12.1)
190
+ ruby2_keywords (0.0.5)
191
+ sidekiq (7.2.0)
192
+ concurrent-ruby (< 2)
193
+ connection_pool (>= 2.3.0)
194
+ rack (>= 2.2.4)
195
+ redis-client (>= 0.14.0)
196
+ stringio (3.1.0)
197
+ thor (1.3.0)
198
+ timecop (0.9.8)
199
+ timeout (0.4.1)
200
+ tzinfo (2.0.6)
201
+ concurrent-ruby (~> 1.0)
202
+ webrick (1.8.1)
203
+ websocket-driver (0.7.6)
204
+ websocket-extensions (>= 0.1.0)
205
+ websocket-extensions (0.1.5)
206
+ zeitwerk (2.6.12)
207
+
208
+ PLATFORMS
209
+ arm64-darwin-21
210
+
211
+ DEPENDENCIES
212
+ rspec
213
+ rspec-goodies!
214
+ sidekiq
215
+ timecop
216
+
217
+ BUNDLED WITH
218
+ 2.4.20
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 James Hu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # rspec-goodies
2
+
3
+ Provides RSpec helpers and matchers for stubs, Sidekiq, and more.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem "rspec-goodies"
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ ```shell
16
+ bundle install
17
+ ```
18
+
19
+ Or install it yourself as:
20
+
21
+ ```shell
22
+ gem install rspec-goodies
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ Within `spec_helper.rb` or `rails_helper.rb`
28
+
29
+ ```ruby
30
+ require "rspec-goodies"
31
+ ```
32
+
33
+ ## Development
34
+
35
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
36
+
37
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
38
+
39
+ ## Contributing
40
+
41
+ Bug reports and pull requests are welcome on GitHub at https://github.com/axsuul/rspec-goodies.
42
+
43
+ ## License
44
+
45
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ task default: %i[]
@@ -0,0 +1,156 @@
1
+ require "active_support"
2
+ require "active_support/core_ext/hash"
3
+
4
+ module RSpec
5
+ module Goodies
6
+ module Helpers
7
+ module Sidekiq
8
+ # Helper to briefly enable unique jobs functionality Only use this if you're explicitly testing unique job
9
+ # behavior otherwise it is very unpredictable and it's best to disable it for normal tests
10
+ def within_sidekiq_unique
11
+ ::Sidekiq::Enterprise.unique!
12
+
13
+ yield
14
+ ensure
15
+ disable_sidekiq_unique!
16
+ end
17
+
18
+ def disable_sidekiq_unique!
19
+ ::Sidekiq.configure_server do |config|
20
+ config.server_middleware do |chain|
21
+ chain.remove(::Sidekiq::Enterprise::Unique::Server)
22
+ end
23
+ end
24
+
25
+ ::Sidekiq.configure_client do |config|
26
+ config.client_middleware do |chain|
27
+ chain.remove(::Sidekiq::Enterprise::Unique::Client)
28
+ end
29
+ end
30
+ end
31
+
32
+ # Simulate job being performed within the entire middleware stack
33
+ def perform_sidekiq_job(worker_class, *args)
34
+ worker_class.perform_async(*args)
35
+
36
+ actual_worker_class =
37
+ if worker_class.is_a?(::Sidekiq::Worker::Setter)
38
+ worker_class.instance_variable_get("@klass")
39
+ else
40
+ worker_class
41
+ end
42
+
43
+ actual_worker_class.process_job(actual_worker_class.jobs.last)
44
+ end
45
+
46
+ def process_sidekiq_payloads(payloads)
47
+ payloads.each do |payload|
48
+ payload_hash = ::Sidekiq.load_json(payload)
49
+ worker_class = ::Sidekiq::Testing.constantize(payload_hash["class"])
50
+
51
+ worker_class.process_job(payload_hash)
52
+ end
53
+ end
54
+
55
+ def add_enqueued_sidekiq_job(worker_class:, args:, metadata: {}, jid: SecureRandom.hex, queue: :default)
56
+ payload = ::Sidekiq.dump_json(
57
+ metadata.merge(
58
+ "jid" => jid,
59
+ "class" => worker_class.to_s,
60
+ "args" => args,
61
+ "queue" => queue.to_s,
62
+ "enqueued_at" => Time.current.to_f,
63
+ ),
64
+ )
65
+
66
+ ::Sidekiq.redis do |redis|
67
+ redis.sadd("queues", queue.to_s)
68
+ redis.lpush("queue:#{queue}", payload)
69
+ end
70
+
71
+ payload
72
+ end
73
+
74
+ # Simulate job being added to retry queue (based off Sidekiq source code)
75
+ def add_retry_sidekiq_job(
76
+ worker_class:,
77
+ args:,
78
+ metadata: {},
79
+ jid: SecureRandom.hex,
80
+ retry_count: 2,
81
+ retry_at: 1.hour.from_now
82
+ )
83
+ payload = ::Sidekiq.dump_json(
84
+ metadata.merge(
85
+ "jid" => jid,
86
+ "class" => worker_class.to_s,
87
+ "args" => args,
88
+ "queue" => "default",
89
+ "failed_at" => Time.now.to_f,
90
+ "retry_count" => retry_count,
91
+ "error_backtrace" => ["line1", "line2"],
92
+ ),
93
+ )
94
+
95
+ ::Sidekiq.redis do |redis|
96
+ redis.zadd("retry", retry_at.to_f.to_s, payload)
97
+ end
98
+
99
+ payload
100
+ end
101
+
102
+ def add_scheduled_sidekiq_job(worker_class:, args:, metadata: {}, jid: SecureRandom.hex)
103
+ payload = ::Sidekiq.dump_json(
104
+ metadata.merge(
105
+ "jid" => jid,
106
+ "class" => worker_class.to_s,
107
+ "args" => args,
108
+ ),
109
+ )
110
+ score = Time.now.to_f
111
+
112
+ ::Sidekiq.redis do |redis|
113
+ redis.zadd("schedule", score, payload)
114
+ end
115
+
116
+ payload
117
+ end
118
+
119
+ # Simulate job being worked on a worker so that's available via the Sidekiq::Workers.new API (based off Sidekiq
120
+ # source code)
121
+ def add_in_progress_sidekiq_job(worker_class:, args:, jid: SecureRandom.hex)
122
+ @sidekiq_job_in_progress_thread_id ||= 1000
123
+ @sidekiq_job_in_progress_count ||= 0
124
+
125
+ process_id = "foo:#{SecureRandom.hex}"
126
+ job_data = ::Sidekiq.dump_json(
127
+ "queue" => "default",
128
+ "payload" => {
129
+ "jid" => jid,
130
+ "class" => worker_class.to_s,
131
+ "args" => args,
132
+ },
133
+ "run_at" => Time.current.to_i,
134
+ )
135
+ process_data = ::Sidekiq.dump_json(
136
+ "hostname" => "foo",
137
+ "started_at" => Time.now.to_f,
138
+ "queues" => ["default"],
139
+ )
140
+
141
+ ::Sidekiq.redis do |redis|
142
+ redis.incr("busy")
143
+ redis.sadd("processes", process_id)
144
+ redis.hmset(
145
+ process_id, "info",
146
+ process_data, "at",
147
+ Time.current.to_f, "busy",
148
+ @sidekiq_job_in_progress_count += 1,
149
+ )
150
+ redis.hmset("#{process_id}:work", @sidekiq_job_in_progress_thread_id += 1, job_data)
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,76 @@
1
+ module RSpec
2
+ module Goodies
3
+ module Helpers
4
+ module Stubs
5
+ def stub_service_as_spy(klass)
6
+ service_stub = class_spy(klass)
7
+ stub_const(klass.to_s, service_stub)
8
+
9
+ service_stub
10
+ end
11
+
12
+ def stub_rails_logger_as_spy
13
+ instance_spy("ActiveSupport::Logger").tap do |stub|
14
+ allow(::Rails).to receive(:logger).and_return(stub)
15
+ end
16
+ end
17
+
18
+ def stub_class_const(klass, const_string, value)
19
+ raise ArgumentError, "a Class or Module must be passed in" if !klass.is_a?(Class) && !klass.is_a?(Module)
20
+
21
+ # Check that constant actually exists. Also by calling klass here we ensure it's loaded before stubbing
22
+ # constant
23
+ unless klass.const_defined?(const_string)
24
+ raise Exception, "Tried to stub #{klass}::#{const_string} but it doesn't exist!"
25
+ end
26
+
27
+ stub_const("#{klass.name}::#{const_string}", value)
28
+ end
29
+
30
+ # Stubs existing constant with resulting hash deep merged with existing hash and hash passed in
31
+ def stub_class_const_and_merge(klass, const_string, hash)
32
+ raise ArgumentError, "must pass in hash" unless hash.is_a?(Hash)
33
+
34
+ existing_hash = klass.const_get(const_string)
35
+
36
+ stub_class_const(klass, const_string, existing_hash.deep_merge(hash))
37
+ end
38
+
39
+ # Stub environment variable so that it doesn't leak out of tests
40
+ def stub_env(name, value)
41
+ allow(ENV).to receive(:[]).with(name).and_return(value)
42
+ end
43
+
44
+ # Only works with nested credentials for now
45
+ def stub_rails_credentials(stubbed_config)
46
+ raise NotImplementedError, "Rails not found" unless Object.const_defined?(:Rails)
47
+
48
+ credentials = ::Rails.application.credentials
49
+ credentials_config = credentials.config
50
+ stubbed_credentials_config = credentials_config
51
+
52
+ stubbed_config.each do |key, key_stubbed_config|
53
+ key = key.to_sym
54
+
55
+ case key_stubbed_config
56
+ when Hash
57
+ key_stubbed_config.each do |child_key, _|
58
+ if !credentials_config.key?(key) || !credentials_config[key].key?(child_key)
59
+ raise ArgumentError, "Tried to stub Rails credential #{key}: #{child_key} but it doesn't exist!"
60
+ end
61
+ end
62
+
63
+ # Merge in existing credentials so we don't break accessing other credentials
64
+ stubbed_credentials_config[key].merge!(key_stubbed_config)
65
+ when String
66
+ # Not nested so set it directly
67
+ stubbed_credentials_config[key] = key_stubbed_config
68
+ end
69
+ end
70
+
71
+ allow(::Rails.application).to receive(:credentials).and_return(OpenStruct.new(stubbed_credentials_config))
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,50 @@
1
+ # Used to match if array includes elements and when order matters
2
+ RSpec::Matchers.define :include_in_order do |*args|
3
+ match do |collection|
4
+ expect_to_match = lambda do |value, matcher|
5
+ # If composable matcher (e.g. a_string_matching(...) then we need to use a different expectation)
6
+ if matcher.class.name.match?(/Matcher/)
7
+ expect(value).to match(matcher)
8
+ else
9
+ expect(value).to eq(matcher)
10
+ end
11
+ end
12
+
13
+ is_matched_in_order = false
14
+
15
+ # Go through collection and try to find the first match
16
+ collection.each_with_index do |element, collection_index|
17
+ initial_matcher = args.first
18
+
19
+ expect_to_match.call(element, initial_matcher)
20
+
21
+ # If reaches here, that means we found the first match so let's see if the remaining also match in order
22
+ is_matched_in_order = args[1..-1].each_with_index.all? do |pending_matcher, pending_matcher_index|
23
+ expect_to_match.call(collection[collection_index + pending_matcher_index + 1], pending_matcher)
24
+
25
+ true
26
+ rescue RSpec::Expectations::ExpectationNotMetError
27
+ false
28
+ end
29
+
30
+ # No need to search anymore once we found it
31
+ break if is_matched_in_order
32
+ rescue RSpec::Expectations::ExpectationNotMetError
33
+ # Keep trying to find the first match
34
+ end
35
+
36
+ is_matched_in_order
37
+ end
38
+ end
39
+
40
+ RSpec::Matchers.alias_matcher :a_collection_including_in_order, :include_in_order
41
+
42
+ # So we can do:
43
+ #
44
+ # expect(collection).to not_any(...)
45
+ #
46
+ # since we can't do:
47
+ #
48
+ # expect(collection).not_to all(...)
49
+ #
50
+ RSpec::Matchers.define_negated_matcher :not_any, :include
@@ -0,0 +1,24 @@
1
+ RSpec::Matchers.define :match_timestamp do |expected, decimal_places = nil|
2
+ normalize = lambda do |value|
3
+ value = DateTime.parse(value) if value.is_a?(String)
4
+
5
+ # Compare floats since there can be sub-seconds
6
+ normalized = value.to_f
7
+
8
+ # Can be limited to decimals places (e.g. 0.002 can match 0.0019 if "3" is provided)
9
+ normalized = normalized.round(decimal_places) if decimal_places
10
+
11
+ normalized
12
+ end
13
+
14
+ match do |actual|
15
+ normalize.call(actual) == normalize.call(expected)
16
+ end
17
+
18
+ failure_message do |actual|
19
+ <<~MESSAGE
20
+ expected that #{actual} (#{normalize.call(actual)}) would match timestamp #{expected}
21
+ (#{normalize.call(expected)})"
22
+ MESSAGE
23
+ end
24
+ end
@@ -0,0 +1,8 @@
1
+ # Used to match if hash doesn't have all keys
2
+ RSpec::Matchers.define :not_have_keys do |*keys|
3
+ match do |hash|
4
+ keys.all? { |k| !hash.key?(k) }
5
+ end
6
+ end
7
+
8
+ RSpec::Matchers.alias_matcher :a_hash_without_keys, :not_have_keys
@@ -0,0 +1,214 @@
1
+ require "rspec/matchers"
2
+ require "sidekiq/testing"
3
+
4
+ class SidekiqJobsEnqueuedMatcher
5
+ include RSpec::Matchers
6
+
7
+ attr_reader :new_jobs, :new_jobs_matching_properties
8
+
9
+ def initialize(actual, worker_class, expected_size = nil, expected_properties = {})
10
+ @actual = actual
11
+ @worker_class = worker_class
12
+
13
+ # Size is optional to be passed in
14
+ if expected_size.is_a?(Hash)
15
+ @expected_properties = expected_size
16
+ @expected_size = nil
17
+ else
18
+ @expected_properties = expected_properties
19
+ @expected_size = expected_size
20
+ end
21
+
22
+ # Normalize
23
+ @expected_properties.stringify_keys!
24
+ end
25
+
26
+ def matches?
27
+ # Get jobs before so that we only perform new jobs
28
+ jobs_before = @worker_class.jobs.clone
29
+
30
+ # Calls the actual block of the matcher
31
+ @actual.call
32
+
33
+ jobs_after = @worker_class.jobs
34
+ @new_jobs = jobs_after - jobs_before
35
+
36
+ @new_jobs_matching_properties = @new_jobs.select do |job|
37
+ matched_count = 0
38
+
39
+ @expected_properties.each do |key, value|
40
+ case key
41
+ when "args"
42
+ # Coerce to an array unless it's a matcher
43
+ value = Array(value) unless value.respond_to?(:base_matcher)
44
+
45
+ expect(Array(job[key])).to match(value)
46
+ when "at"
47
+ if (scheduled_at = job[key])
48
+ # It's a float in job
49
+ expect(scheduled_at).to eq(value.to_f)
50
+
51
+ # Otherwise if nil, then we are looking for jobs that aren't scheduled
52
+ else
53
+ expect(job.key?("at")).to eq false
54
+ end
55
+ when "bid"
56
+ expect(job[key]).to match(value)
57
+ when "queue", "unique_for", "unique_until"
58
+ expect(job[key].to_s).to eq(value.to_s)
59
+ when "metadata"
60
+ # Even though we call this metadata, it's actually just keys in the job hash. We iterate through each key so
61
+ # that it also works on checking for nil values
62
+ value.each do |metadata_key, metadata_value|
63
+ expect(job[metadata_key]).to eq metadata_value
64
+ end
65
+ end
66
+
67
+ matched_count += 1
68
+ rescue RSpec::Expectations::ExpectationNotMetError
69
+ # Doesn't contribute to matched_count if any expectations fail
70
+ end
71
+
72
+ matched_count == @expected_properties.count
73
+ end
74
+
75
+ # Check expected number of new jobs enqueued to match size if specified
76
+ if @expected_size
77
+ @new_jobs_matching_properties.size == @expected_size
78
+ else
79
+ @new_jobs_matching_properties.any?
80
+ end
81
+ end
82
+
83
+ def new_jobs_sanitized
84
+ (@new_jobs || []).map { |j| j.except("jid", "backtrace", "retry", "created_at", "enqueued_at") }
85
+ end
86
+ end
87
+
88
+ RSpec::Matchers.define :have_sidekiq_jobs_enqueued do |*args|
89
+ match do |actual|
90
+ @matcher = SidekiqJobsEnqueuedMatcher.new(actual, *args)
91
+
92
+ @matcher.matches?
93
+ end
94
+
95
+ failure_message do |_|
96
+ "expected Sidekiq jobs to be enqueued with #{args.join(', ')} but instead found:\n\n#{@matcher.new_jobs_sanitized.pretty_inspect}"
97
+ end
98
+
99
+ def supports_block_expectations?
100
+ true
101
+ end
102
+ end
103
+
104
+ RSpec::Matchers.alias_matcher :have_sidekiq_job_enqueued, :have_sidekiq_jobs_enqueued
105
+ RSpec::Matchers.define_negated_matcher :not_have_sidekiq_jobs_enqueued, :have_sidekiq_jobs_enqueued
106
+
107
+ RSpec::Matchers.define :have_sidekiq_jobs_enqueued_and_performed do |*args|
108
+ match do |actual|
109
+ # Add the first arguments to the end since we are processing the chain from
110
+ # last to first
111
+ @worker_chain ||= []
112
+ @worker_chain << args
113
+
114
+ # Alias
115
+ perform_jobs = actual
116
+
117
+ while @worker_chain.any?
118
+ args = @worker_chain.pop
119
+ worker_class = args.first
120
+
121
+ @matcher = SidekiqJobsEnqueuedMatcher.new(perform_jobs, *args)
122
+
123
+ expect(@matcher.matches?).to eq true
124
+
125
+ new_jobs_matching_properties = @matcher.new_jobs_matching_properties
126
+
127
+ perform_jobs = lambda do
128
+ new_jobs_matching_properties.each do |job|
129
+ worker_class = job["class"]
130
+
131
+ # Remove from queue after performing it
132
+ Sidekiq::Queues.delete_for(job["jid"], job["queue"], worker_class)
133
+
134
+ worker_class.constantize.process_job(job)
135
+ end
136
+ end
137
+ end
138
+
139
+ # Make sure we call perform jobs one last time if there's no more in chain
140
+ perform_jobs.call
141
+ end
142
+
143
+ chain :thereafter do |*next_args|
144
+ @worker_chain ||= []
145
+ @worker_chain.prepend(next_args)
146
+ end
147
+
148
+ failure_message do |_|
149
+ "expected Sidekiq jobs to be enqueued and performed with #{args.join(', ')} but instead found:\n\n#{@matcher.new_jobs_sanitized.pretty_inspect}"
150
+ end
151
+
152
+ def supports_block_expectations?
153
+ true
154
+ end
155
+ end
156
+
157
+ RSpec::Matchers.define :have_sidekiq_batch_with_callback_triggered do |event, callback_class|
158
+ match do |actual|
159
+ received_callbacks = Hash.new do |hash, key|
160
+ hash[key] = {
161
+ death: [],
162
+ complete: [],
163
+ success: [],
164
+ }
165
+ end
166
+
167
+ sidekiq_batch_stub = ::Sidekiq::Batch.new.tap do |stub|
168
+ # This can be called multiple times so track each time
169
+ allow(stub).to receive(:on) do |actual_event, actual_callback_class, actual_callback_options|
170
+ received_callbacks[actual_callback_class.name][actual_event].push(actual_callback_options)
171
+ end
172
+
173
+ allow(::Sidekiq::Batch).to receive(:new).and_return(stub)
174
+ end
175
+
176
+ # Ensure stuff happens within batch
177
+ allow(sidekiq_batch_stub).to receive(:jobs).and_call_original
178
+
179
+ actual.call
180
+
181
+ # Only care about those received for event and callback
182
+ relevant_received_callbacks = received_callbacks[callback_class.to_s][event]
183
+
184
+ expect(relevant_received_callbacks).to be_any
185
+
186
+ # Once block is complete, simulate Sidekiq batch callback is called by manually calling it ourselves since it
187
+ # doesn't automatically get called in tests
188
+ ::Sidekiq::Batch.new.tap do |batch|
189
+ # Ensure created in redis so that we can access it
190
+ batch.jobs {}
191
+
192
+ # Simulate that callback is called for each time callback options were received
193
+ relevant_received_callbacks.each do |callback_options|
194
+ # Also simulate that callback options are converted to JSON and back to hash which is what happens in production
195
+ callback_class.new.send(
196
+ :"on_#{event}",
197
+ ::Sidekiq::Batch::Status.new(batch.bid),
198
+ JSON.parse(callback_options.to_json),
199
+ )
200
+ end
201
+ end
202
+ end
203
+
204
+ failure_message do |_|
205
+ "expected jobs to perform within Sidekiq batch with #{callback_class} callback to be triggered by #{event}"
206
+ end
207
+
208
+ def supports_block_expectations?
209
+ true
210
+ end
211
+ end
212
+
213
+ RSpec::Matchers.define_negated_matcher :not_have_sidekiq_batch_with_callback_triggered,
214
+ :have_sidekiq_batch_with_callback_triggered
@@ -0,0 +1,16 @@
1
+ RSpec::Matchers.define_negated_matcher :a_string_not_matching, :a_string_matching
2
+
3
+ RSpec::Matchers.define :match_url do |matched_url|
4
+ match do |url|
5
+ uri = Addressable::URI.parse(url)
6
+ matched_uri = Addressable::URI.parse(matched_url)
7
+
8
+ uri.host == matched_uri.host &&
9
+ uri.path == matched_uri.path &&
10
+ uri.query_values == matched_uri.query_values
11
+ end
12
+
13
+ failure_message do |url|
14
+ "expected the url: #{url} to match the url: #{matched_url}"
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec/core"
4
+ require "rspec/goodies/helpers/sidekiq"
5
+ require "rspec/goodies/helpers/stubs"
6
+ require "rspec/goodies/matchers/collection"
7
+ require "rspec/goodies/matchers/date_time"
8
+ require "rspec/goodies/matchers/hash"
9
+ require "rspec/goodies/matchers/sidekiq"
10
+ require "rspec/goodies/matchers/string"
11
+
12
+ RSpec.configure do |config|
13
+ config.include(RSpec::Goodies::Helpers::Sidekiq)
14
+ config.include(RSpec::Goodies::Helpers::Stubs)
15
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "rspec-goodies"
5
+ spec.version = "0.0.1"
6
+ spec.authors = ["James Hu"]
7
+
8
+ spec.summary = "RSpec goodies full of helpers, matchers, and more"
9
+ spec.description = "RSpec goodies full of helpers, matchers, and more"
10
+ spec.homepage = "https://github.com/axsuul/rspec-goodies"
11
+ spec.license = "MIT"
12
+
13
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
14
+ spec.metadata["homepage_uri"] = spec.homepage
15
+ spec.metadata["source_code_uri"] = "https://github.com/axsuul/rspec-goodies"
16
+
17
+ # Specify which files should be added to the gem when it is released.
18
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
19
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
20
+ `git ls-files -z`.split("\x0").reject do |f|
21
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
22
+ end
23
+ end
24
+ spec.bindir = "exe"
25
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
26
+ spec.require_paths = ["lib"]
27
+
28
+ # Uncomment to register a new dependency of your gem
29
+ spec.add_dependency "activesupport"
30
+ spec.add_dependency "byebug"
31
+ spec.add_dependency "rails"
32
+
33
+ spec.add_development_dependency "rspec"
34
+ spec.add_development_dependency "sidekiq"
35
+ spec.add_development_dependency "timecop"
36
+
37
+ # For more information and examples about making a new gem, check out our
38
+ # guide at: https://bundler.io/guides/creating_gem.html
39
+ end
metadata ADDED
@@ -0,0 +1,143 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rspec-goodies
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - James Hu
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-01-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: byebug
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rails
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: sidekiq
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: timecop
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: RSpec goodies full of helpers, matchers, and more
98
+ email:
99
+ executables: []
100
+ extensions: []
101
+ extra_rdoc_files: []
102
+ files:
103
+ - Gemfile
104
+ - Gemfile.lock
105
+ - LICENSE.txt
106
+ - README.md
107
+ - Rakefile
108
+ - lib/rspec-goodies.rb
109
+ - lib/rspec/goodies/helpers/sidekiq.rb
110
+ - lib/rspec/goodies/helpers/stubs.rb
111
+ - lib/rspec/goodies/matchers/collection.rb
112
+ - lib/rspec/goodies/matchers/date_time.rb
113
+ - lib/rspec/goodies/matchers/hash.rb
114
+ - lib/rspec/goodies/matchers/sidekiq.rb
115
+ - lib/rspec/goodies/matchers/string.rb
116
+ - rspec-goodies.gemspec
117
+ homepage: https://github.com/axsuul/rspec-goodies
118
+ licenses:
119
+ - MIT
120
+ metadata:
121
+ allowed_push_host: https://rubygems.org
122
+ homepage_uri: https://github.com/axsuul/rspec-goodies
123
+ source_code_uri: https://github.com/axsuul/rspec-goodies
124
+ post_install_message:
125
+ rdoc_options: []
126
+ require_paths:
127
+ - lib
128
+ required_ruby_version: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ required_rubygems_version: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ requirements: []
139
+ rubygems_version: 3.1.6
140
+ signing_key:
141
+ specification_version: 4
142
+ summary: RSpec goodies full of helpers, matchers, and more
143
+ test_files: []