shoulda-matchers 5.2.0 → 6.0.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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +22 -7
  4. data/lib/shoulda/matchers/action_controller/permit_matcher.rb +1 -1
  5. data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +9 -0
  6. data/lib/shoulda/matchers/active_model/comparison_matcher.rb +162 -0
  7. data/lib/shoulda/matchers/active_model/numericality_matchers/numeric_type_matcher.rb +3 -5
  8. data/lib/shoulda/matchers/active_model/numericality_matchers/range_matcher.rb +71 -0
  9. data/lib/shoulda/matchers/active_model/numericality_matchers/submatchers.rb +58 -0
  10. data/lib/shoulda/matchers/active_model/validate_absence_of_matcher.rb +2 -1
  11. data/lib/shoulda/matchers/active_model/validate_comparison_of_matcher.rb +534 -0
  12. data/lib/shoulda/matchers/active_model/validate_exclusion_of_matcher.rb +5 -5
  13. data/lib/shoulda/matchers/active_model/validate_inclusion_of_matcher.rb +9 -6
  14. data/lib/shoulda/matchers/active_model/validate_length_of_matcher.rb +64 -9
  15. data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +72 -80
  16. data/lib/shoulda/matchers/active_model/validation_matcher.rb +6 -0
  17. data/lib/shoulda/matchers/active_model/validator.rb +4 -0
  18. data/lib/shoulda/matchers/active_model.rb +4 -1
  19. data/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb +0 -8
  20. data/lib/shoulda/matchers/active_record/have_db_column_matcher.rb +46 -6
  21. data/lib/shoulda/matchers/active_record/have_db_index_matcher.rb +14 -3
  22. data/lib/shoulda/matchers/active_record/have_implicit_order_column.rb +3 -5
  23. data/lib/shoulda/matchers/active_record/normalize_matcher.rb +151 -0
  24. data/lib/shoulda/matchers/active_record.rb +1 -0
  25. data/lib/shoulda/matchers/independent/delegate_method_matcher.rb +1 -1
  26. data/lib/shoulda/matchers/rails_shim.rb +10 -8
  27. data/lib/shoulda/matchers/util/word_wrap.rb +2 -2
  28. data/lib/shoulda/matchers/util.rb +1 -1
  29. data/lib/shoulda/matchers/version.rb +1 -1
  30. data/lib/shoulda/matchers.rb +2 -2
  31. data/shoulda-matchers.gemspec +1 -1
  32. metadata +12 -8
  33. data/lib/shoulda/matchers/active_model/numericality_matchers/comparison_matcher.rb +0 -157
@@ -0,0 +1,534 @@
1
+ module Shoulda
2
+ module Matchers
3
+ module ActiveModel
4
+ # The `validate_comparison_of` matcher tests usage of the
5
+ # `validates_comparison_of` validation.
6
+ #
7
+ # class Person
8
+ # include ActiveModel::Model
9
+ # attr_accessor :gpa
10
+ #
11
+ # validates_comparison_of :gpa, greater_than: 10
12
+ # end
13
+ #
14
+ # # RSpec
15
+ # RSpec.describe Person, type: :model do
16
+ # it { should validate_comparison_of(:gpa).greater_than(10) }
17
+ # end
18
+ #
19
+ # # Minitest (Shoulda)
20
+ # class PersonTest < ActiveSupport::TestCase
21
+ # should validate_comparison_of(:gpa).greater_than(10)
22
+ # end
23
+ #
24
+ # #### Qualifiers
25
+ #
26
+ # ##### on
27
+ #
28
+ # Use `on` if your validation applies only under a certain context.
29
+ #
30
+ # class Person
31
+ # include ActiveModel::Model
32
+ # attribute :number_of_dependents, :integer
33
+ # attr_accessor :number_of_dependents
34
+ #
35
+ # validates_comparison_of :number_of_dependents, on: :create, greater_than: 0
36
+ # end
37
+ #
38
+ # # RSpec
39
+ # RSpec.describe Person, type: :model do
40
+ # it do
41
+ # should validate_comparison_of(:number_of_dependents).
42
+ # greater_than(0).
43
+ # on(:create)
44
+ # end
45
+ # end
46
+ #
47
+ # # Minitest (Shoulda)
48
+ # class PersonTest < ActiveSupport::TestCase
49
+ # should validate_comparison_of(:number_of_dependents).greater_than(0).on(:create)
50
+ # end
51
+ #
52
+ # ##### is_less_than
53
+ #
54
+ # Use `is_less_than` to test usage of the the `:less_than` option. This
55
+ # asserts that the attribute can take a value which is less than the
56
+ # given value and cannot take a value which is greater than or equal to
57
+ # it. It can also accept methods or procs that returns a given value.
58
+ #
59
+ # class Person
60
+ # include ActiveModel::Model
61
+ # attribute :number_of_cars, :integer
62
+ # attr_accessor :number_of_cars
63
+ #
64
+ # validates_comparison_of :number_of_cars, less_than: :current_number_of_cars
65
+ #
66
+ # def current_number_of_cars
67
+ # 10
68
+ # end
69
+ # end
70
+ #
71
+ # # RSpec
72
+ # RSpec.describe Person, type: :model do
73
+ # it do
74
+ # should validate_comparison_of(:number_of_cars).
75
+ # is_less_than(:current_number_of_cars)
76
+ # end
77
+ # end
78
+ #
79
+ # # Minitest (Shoulda)
80
+ # class PersonTest < ActiveSupport::TestCase
81
+ # should validate_comparison_of(:number_of_cars).
82
+ # is_less_than(:current_number_of_cars)
83
+ # end
84
+ #
85
+ # ##### is_less_than_or_equal_to
86
+ #
87
+ # Use `is_less_than_or_equal_to` to test usage of the
88
+ # `:less_than_or_equal_to` option. This asserts that the attribute can
89
+ # take a value which is less than or equal to the given value and cannot
90
+ # take a value which is greater than it. It can also accept methods or
91
+ # procs that returns a given value.
92
+ #
93
+ # class Person
94
+ # include ActiveModel::Model
95
+ # attr_accessor :birth_date
96
+ #
97
+ # validates_comparison_of :birth_date, less_than_or_equal_to: Date.new(1987, 12, 31)
98
+ # end
99
+ #
100
+ # # RSpec
101
+ # RSpec.describe Person, type: :model do
102
+ # it do
103
+ # should validate_comparison_of(:birth_date).
104
+ # is_less_than_or_equal_to(Date.new(1987, 12, 31))
105
+ # end
106
+ # end
107
+ #
108
+ # # Minitest (Shoulda)
109
+ # class PersonTest < ActiveSupport::TestCase
110
+ # should validate_comparison_of(:birth_date).
111
+ # is_less_than_or_equal_to(Date.new(1987, 12, 31))
112
+ # end
113
+ #
114
+ # ##### is_greater_than_or_equal_to
115
+ #
116
+ # Use `is_greater_than_or_equal_to` to test usage of the
117
+ # `:greater_than_or_equal_to` option. This asserts that the attribute can
118
+ # take a value which is greater than or equal to the given value and
119
+ # cannot take a value which is less than it.
120
+ #
121
+ # class Person
122
+ # include ActiveModel::Model
123
+ # attribute :birth_date, :date
124
+ # attr_accessor :birth_date
125
+ #
126
+ # validates_comparison_of :birth_date,
127
+ # greater_than_or_equal_to: -> { 18.years.ago.to_date }
128
+ # end
129
+ #
130
+ # # RSpec
131
+ # RSpec.describe Person, type: :model do
132
+ # it do
133
+ # should validate_comparison_of(:birth_date).
134
+ # is_greater_than_or_equal_to(-> { 18.years.ago.to_date })
135
+ # end
136
+ # end
137
+ #
138
+ # # Minitest (Shoulda)
139
+ # class PersonTest < ActiveSupport::TestCase
140
+ # should validate_comparison_of(:birth_date).
141
+ # is_greater_than_or_equal_to(-> { 18.years.ago.to_date })
142
+ # end
143
+ #
144
+ # ##### is_greater_than
145
+ #
146
+ # Use `is_greater_than` to test usage of the `:greater_than` option.
147
+ # This asserts that the attribute can take a value which is greater than
148
+ # the given value and cannot take a value less than or equal to it.
149
+ # It can also accept methods or procs that returns a given value.
150
+ #
151
+ # class Person
152
+ # include ActiveModel::Model
153
+ # attribute :legal_age, :integer
154
+ # attr_accessor :legal_age
155
+ #
156
+ # validates_comparison_of :legal_age, greater_than: 21
157
+ # end
158
+ #
159
+ # # RSpec
160
+ # RSpec.describe Person, type: :model do
161
+ # it do
162
+ # should validate_comparison_of(:legal_age).
163
+ # is_greater_than(21)
164
+ # end
165
+ # end
166
+ #
167
+ # # Minitest (Shoulda)
168
+ # class PersonTest < ActiveSupport::TestCase
169
+ # should validate_comparison_of(:legal_age).
170
+ # is_greater_than(21)
171
+ # end
172
+ #
173
+ # ##### is_equal_to
174
+ #
175
+ # Use `is_equal_to` to test usage of the `:equal_to` option. This asserts
176
+ # that the attribute can take a value which is equal to the given value
177
+ # and cannot take a value which is not equal. It can also accept methods or
178
+ # procs that returns a given value.
179
+ #
180
+ # class Person
181
+ # include ActiveModel::Model
182
+ # attribute :favorite_color, :string
183
+ # attr_accessor :favorite_color
184
+ #
185
+ # validates_comparison_of :favorite_color, equal_to: "blue"
186
+ # end
187
+ #
188
+ # # RSpec
189
+ # RSpec.describe Person, type: :model do
190
+ # it { should validate_comparison_of(:favorite_color).is_equal_to("blue") }
191
+ # end
192
+ #
193
+ # # Minitest (Shoulda)
194
+ # class PersonTest < ActiveSupport::TestCase
195
+ # should validate_comparison_of(:favorite_color).is_equal_to("blue")
196
+ # end
197
+ #
198
+ #
199
+ # ##### is_other_than
200
+ #
201
+ # Use `is_other_than` to test usage of the `:other_than` option.
202
+ # This asserts that the attribute can take a number which is not equal to
203
+ # the given value.
204
+ #
205
+ # class Person
206
+ # include ActiveModel::Model
207
+ # attr_accessor :legal_age
208
+ #
209
+ # validates_comparison_of :legal_age, other_than: 21
210
+ # end
211
+ #
212
+ # # RSpec
213
+ # RSpec.describe Person, type: :model do
214
+ # it do
215
+ # should validate_comparison_of(:legal_age).
216
+ # is_other_than(21)
217
+ # end
218
+ # end
219
+ #
220
+ # # Minitest (Shoulda)
221
+ # class PersonTest < ActiveSupport::TestCase
222
+ # should validate_comparison_of(:legal_age).
223
+ # is_other_than(21)
224
+ # end
225
+ #
226
+ # ##### with_message
227
+ #
228
+ # Use `with_message` if you are using a custom validation message.
229
+ #
230
+ # class Person
231
+ # include ActiveModel::Model
232
+ # attr_accessor :number_of_dependents
233
+ #
234
+ # validates_comparison_of :number_of_dependents, greater_than: 0
235
+ # message: 'Number of dependents must be a number'
236
+ # end
237
+ #
238
+ # # RSpec
239
+ # RSpec.describe Person, type: :model do
240
+ # it do
241
+ # should validate_comparison_of(:number_of_dependents).
242
+ # is_greater_than(0).
243
+ # with_message('Number of dependents must be a number')
244
+ # end
245
+ # end
246
+ #
247
+ # # Minitest (Shoulda)
248
+ # class PersonTest < ActiveSupport::TestCase
249
+ # should validate_comparison_of(:number_of_dependents).
250
+ # is_greater_than(0).
251
+ # with_message('Number of dependents must be a number')
252
+ # end
253
+ #
254
+ # ##### allow_nil
255
+ #
256
+ # Use `allow_nil` to assert that the attribute allows nil.
257
+ #
258
+ # class Post
259
+ # include ActiveModel::Model
260
+ # attr_accessor :age
261
+ #
262
+ # validates_comparison_of :age, greater_than: 0, allow_nil: true
263
+ # end
264
+ #
265
+ # # RSpec
266
+ # RSpec.describe Post, type: :model do
267
+ # it { should validate_comparison_of(:age).is_greater_than(0).allow_nil }
268
+ # end
269
+ #
270
+ # # Minitest (Shoulda)
271
+ # class PostTest < ActiveSupport::TestCase
272
+ # should validate_comparison_of(:age).is_greater_than(0).allow_nil
273
+ # end
274
+ #
275
+ # @return [ValidateComparisonOfMatcher]
276
+ #
277
+ def validate_comparison_of(attr)
278
+ ValidateComparisonOfMatcher.new(attr)
279
+ end
280
+
281
+ # @private
282
+ class ValidateComparisonOfMatcher < ValidationMatcher
283
+ NUMERIC_NAME = 'number'.freeze
284
+ DEFAULT_DIFF_TO_COMPARE = 1
285
+
286
+ attr_reader :diff_to_compare, :number_of_submatchers
287
+
288
+ def initialize(attribute)
289
+ super
290
+ @submatchers = []
291
+ @diff_to_compare = DEFAULT_DIFF_TO_COMPARE
292
+ @expects_to_allow_nil = false
293
+ @comparison_submatcher = false
294
+ end
295
+
296
+ def allow_nil
297
+ @expects_to_allow_nil = true
298
+ prepare_submatcher(allow_value_matcher(nil))
299
+ self
300
+ end
301
+
302
+ def expects_to_allow_nil?
303
+ @expects_to_allow_nil
304
+ end
305
+
306
+ def is_greater_than(value)
307
+ prepare_submatcher(comparison_matcher_for(value, :>).for(attribute))
308
+ self
309
+ end
310
+
311
+ def is_greater_than_or_equal_to(value)
312
+ prepare_submatcher(comparison_matcher_for(value, :>=).for(attribute))
313
+ self
314
+ end
315
+
316
+ def is_equal_to(value)
317
+ prepare_submatcher(comparison_matcher_for(value, :==).for(attribute))
318
+ self
319
+ end
320
+
321
+ def is_less_than(value)
322
+ prepare_submatcher(comparison_matcher_for(value, :<).for(attribute))
323
+ self
324
+ end
325
+
326
+ def is_less_than_or_equal_to(value)
327
+ prepare_submatcher(comparison_matcher_for(value, :<=).for(attribute))
328
+ self
329
+ end
330
+
331
+ def is_other_than(value)
332
+ prepare_submatcher(comparison_matcher_for(value, :!=).for(attribute))
333
+ self
334
+ end
335
+
336
+ def matches?(subject)
337
+ @subject = subject
338
+ @number_of_submatchers = @submatchers.size
339
+ unless @comparison_matcher
340
+ raise(ArgumentError, "matcher isn't qualified with any comparison matcher")
341
+ end
342
+
343
+ qualify_submatchers
344
+ first_submatcher_that_fails_to_match.nil?
345
+ end
346
+
347
+ def does_not_match?(subject)
348
+ @subject = subject
349
+ @number_of_submatchers = @submatchers.size
350
+
351
+ qualify_submatchers
352
+ first_submatcher_that_fails_to_not_match.nil?
353
+ end
354
+
355
+ def simple_description
356
+ description = ''
357
+
358
+ description << "validate that :#{attribute} looks like "
359
+ description << Shoulda::Matchers::Util.a_or_an(allowed_type_name)
360
+
361
+ if comparison_descriptions.present?
362
+ description << " #{comparison_descriptions}"
363
+ end
364
+
365
+ description
366
+ end
367
+
368
+ def failure_message
369
+ overall_failure_message.dup.tap do |message|
370
+ message << "\n"
371
+ message << failure_message_for_first_submatcher_that_fails_to_match
372
+ end
373
+ end
374
+
375
+ def failure_message_when_negated
376
+ overall_failure_message_when_negated.dup.tap do |message|
377
+ message << "\n"
378
+ message <<
379
+ failure_message_for_first_submatcher_that_fails_to_not_match
380
+ end
381
+ end
382
+
383
+ def given_numeric_column?
384
+ attribute_is_active_record_column? &&
385
+ [:integer, :float, :decimal].include?(column_type)
386
+ end
387
+
388
+ private
389
+
390
+ def attribute_is_active_record_column?
391
+ columns_hash.key?(attribute.to_s)
392
+ end
393
+
394
+ def column_type
395
+ columns_hash[attribute.to_s].type
396
+ end
397
+
398
+ def columns_hash
399
+ if subject.class.respond_to?(:columns_hash)
400
+ subject.class.columns_hash
401
+ else
402
+ {}
403
+ end
404
+ end
405
+
406
+ def prepare_submatcher(submatcher)
407
+ add_submatcher(submatcher)
408
+ submatcher
409
+ end
410
+
411
+ def comparison_matcher_for(value, operator)
412
+ @comparison_matcher = true
413
+ ComparisonMatcher.
414
+ new(self, value, operator).
415
+ for(attribute)
416
+ end
417
+
418
+ def add_submatcher(submatcher)
419
+ @submatchers << submatcher
420
+ end
421
+
422
+ def qualify_submatchers
423
+ @submatchers.each do |submatcher|
424
+ if @expects_strict
425
+ submatcher.strict
426
+ end
427
+
428
+ if @expected_message.present?
429
+ submatcher.with_message(@expected_message)
430
+ end
431
+
432
+ if @context
433
+ submatcher.on(@context)
434
+ end
435
+
436
+ submatcher.ignoring_interference_by_writer(
437
+ ignore_interference_by_writer,
438
+ )
439
+ end
440
+ end
441
+
442
+ def number_of_submatchers_for_failure_message
443
+ if has_been_qualified?
444
+ number_of_submatchers - 1
445
+ else
446
+ number_of_submatchers
447
+ end
448
+ end
449
+
450
+ def has_been_qualified?
451
+ @submatchers.any? { |submatcher| submatcher_qualified?(submatcher) }
452
+ end
453
+
454
+ def submatcher_qualified?(submatcher)
455
+ submatcher.instance_of?(ComparisonMatcher)
456
+ end
457
+
458
+ def first_submatcher_that_fails_to_match
459
+ @_first_submatcher_that_fails_to_match ||=
460
+ @submatchers.detect do |submatcher|
461
+ !submatcher.matches?(subject)
462
+ end
463
+ end
464
+
465
+ def first_submatcher_that_fails_to_not_match
466
+ @_first_submatcher_that_fails_to_not_match ||=
467
+ @submatchers.detect do |submatcher|
468
+ !submatcher.does_not_match?(subject)
469
+ end
470
+ end
471
+
472
+ def failure_message_for_first_submatcher_that_fails_to_match
473
+ build_submatcher_failure_message_for(
474
+ first_submatcher_that_fails_to_match,
475
+ :failure_message,
476
+ )
477
+ end
478
+
479
+ def failure_message_for_first_submatcher_that_fails_to_not_match
480
+ build_submatcher_failure_message_for(
481
+ first_submatcher_that_fails_to_not_match,
482
+ :failure_message_when_negated,
483
+ )
484
+ end
485
+
486
+ def build_submatcher_failure_message_for(
487
+ submatcher,
488
+ failure_message_method
489
+ )
490
+ failure_message = submatcher.public_send(failure_message_method)
491
+ submatcher_description = submatcher.simple_description.
492
+ sub(/\bvalidate that\b/, 'validates').
493
+ sub(/\bdisallow\b/, 'disallows').
494
+ sub(/\ballow\b/, 'allows')
495
+ submatcher_message =
496
+ if number_of_submatchers_for_failure_message > 1
497
+ "In checking that #{model.name} #{submatcher_description}, " +
498
+ failure_message[0].downcase +
499
+ failure_message[1..]
500
+ else
501
+ failure_message
502
+ end
503
+
504
+ Shoulda::Matchers.word_wrap(submatcher_message, indent: 2)
505
+ end
506
+
507
+ def comparison_descriptions
508
+ description_array = submatcher_comparison_descriptions
509
+ if description_array.empty?
510
+ ''
511
+ else
512
+ submatcher_comparison_descriptions.join(' and ')
513
+ end
514
+ end
515
+
516
+ def submatcher_comparison_descriptions
517
+ @submatchers.inject([]) do |arr, submatcher|
518
+ arr << if submatcher.respond_to? :comparison_description
519
+ submatcher.comparison_description
520
+ end
521
+ end
522
+ end
523
+
524
+ def allowed_type_name
525
+ 'value'
526
+ end
527
+
528
+ def non_numeric_value
529
+ 'abcd'
530
+ end
531
+ end
532
+ end
533
+ end
534
+ end
@@ -3,10 +3,10 @@ module Shoulda
3
3
  module ActiveModel
4
4
  # The `validate_exclusion_of` matcher tests usage of the
5
5
  # `validates_exclusion_of` validation, asserting that an attribute cannot
6
- # take a blacklist of values, and inversely, can take values outside of
6
+ # take a blocklist of values, and inversely, can take values outside of
7
7
  # this list.
8
8
  #
9
- # If your blacklist is an array of values, use `in_array`:
9
+ # If your blocklist an array of values, use `in_array`:
10
10
  #
11
11
  # class Game
12
12
  # include ActiveModel::Model
@@ -29,13 +29,13 @@ module Shoulda
29
29
  # in_array(['Mac', 'Linux'])
30
30
  # end
31
31
  #
32
- # If your blacklist is a range of values, use `in_range`:
32
+ # If your blocklist is a range of values, use `in_range`:
33
33
  #
34
34
  # class Game
35
35
  # include ActiveModel::Model
36
- # attr_accessor :supported_os
36
+ # attr_accessor :floors_with_enemies
37
37
  #
38
- # validates_exclusion_of :supported_os, in: 5..8
38
+ # validates_exclusion_of :floors_with_enemies, in: 5..8
39
39
  # end
40
40
  #
41
41
  # # RSpec
@@ -3,12 +3,15 @@ require 'date'
3
3
 
4
4
  module Shoulda
5
5
  module Matchers
6
+ # @private
7
+ class ExampleClass; end
8
+
6
9
  module ActiveModel
7
10
  # The `validate_inclusion_of` matcher tests usage of the
8
11
  # `validates_inclusion_of` validation, asserting that an attribute can
9
- # take a whitelist of values and cannot take values outside of this list.
12
+ # take a allowlist of values and cannot take values outside of this list.
10
13
  #
11
- # If your whitelist is an array of values, use `in_array`:
14
+ # If your allowlist is an array of values, use `in_array`:
12
15
  #
13
16
  # class Issue
14
17
  # include ActiveModel::Model
@@ -32,7 +35,7 @@ module Shoulda
32
35
  # in_array(['open', 'resolved', 'unresolved'])
33
36
  # end
34
37
  #
35
- # If your whitelist is a range of values, use `in_range`:
38
+ # If your allowlist is a range of values, use `in_range`:
36
39
  #
37
40
  # class Issue
38
41
  # include ActiveModel::Model
@@ -43,12 +46,12 @@ module Shoulda
43
46
  #
44
47
  # # RSpec
45
48
  # RSpec.describe Issue, type: :model do
46
- # it { should validate_inclusion_of(:state).in_range(1..5) }
49
+ # it { should validate_inclusion_of(:priority).in_range(1..5) }
47
50
  # end
48
51
  #
49
52
  # # Minitest (Shoulda)
50
53
  # class IssueTest < ActiveSupport::TestCase
51
- # should validate_inclusion_of(:state).in_range(1..5)
54
+ # should validate_inclusion_of(:priority).in_range(1..5)
52
55
  # end
53
56
  #
54
57
  # #### Caveats
@@ -269,7 +272,7 @@ module Shoulda
269
272
  # @private
270
273
  class ValidateInclusionOfMatcher < ValidationMatcher
271
274
  BLANK_VALUES = ['', ' ', "\n", "\r", "\t", "\f"].freeze
272
- ARBITRARY_OUTSIDE_STRING = 'shoulda-matchers test string'.freeze
275
+ ARBITRARY_OUTSIDE_STRING = Shoulda::Matchers::ExampleClass.name
273
276
  ARBITRARY_OUTSIDE_INTEGER = 123456789
274
277
  ARBITRARY_OUTSIDE_DECIMAL = BigDecimal('0.123456789')
275
278
  ARBITRARY_OUTSIDE_DATE = Date.jd(9999999)