shoulda-matchers 6.5.0 → 8.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +9 -5
  3. data/lib/shoulda/matchers/action_controller/render_with_layout_matcher.rb +1 -5
  4. data/lib/shoulda/matchers/action_controller/route_matcher.rb +2 -2
  5. data/lib/shoulda/matchers/action_controller/route_params.rb +1 -1
  6. data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setter.rb +4 -4
  7. data/lib/shoulda/matchers/active_model/allow_value_matcher/attribute_setter_and_validator.rb +5 -5
  8. data/lib/shoulda/matchers/active_model/allow_value_matcher.rb +3 -3
  9. data/lib/shoulda/matchers/active_model/errors.rb +2 -2
  10. data/lib/shoulda/matchers/active_model/have_secure_password_matcher.rb +1 -1
  11. data/lib/shoulda/matchers/active_model/helpers.rb +3 -1
  12. data/lib/shoulda/matchers/active_model/validate_absence_of_matcher.rb +25 -2
  13. data/lib/shoulda/matchers/active_model/validate_acceptance_of_matcher.rb +25 -2
  14. data/lib/shoulda/matchers/active_model/validate_comparison_of_matcher.rb +46 -11
  15. data/lib/shoulda/matchers/active_model/validate_confirmation_of_matcher.rb +26 -3
  16. data/lib/shoulda/matchers/active_model/validate_exclusion_of_matcher.rb +25 -2
  17. data/lib/shoulda/matchers/active_model/validate_inclusion_of_matcher.rb +47 -18
  18. data/lib/shoulda/matchers/active_model/validate_length_of_matcher.rb +26 -3
  19. data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +46 -11
  20. data/lib/shoulda/matchers/active_model/validate_presence_of_matcher.rb +43 -17
  21. data/lib/shoulda/matchers/active_model/validation_matcher.rb +9 -12
  22. data/lib/shoulda/matchers/active_model/validator.rb +6 -2
  23. data/lib/shoulda/matchers/active_record/association_matcher.rb +119 -2
  24. data/lib/shoulda/matchers/active_record/association_matchers/optional_matcher.rb +1 -3
  25. data/lib/shoulda/matchers/active_record/association_matchers/required_matcher.rb +1 -3
  26. data/lib/shoulda/matchers/active_record/define_enum_for_matcher.rb +7 -1
  27. data/lib/shoulda/matchers/active_record/have_attached_matcher.rb +164 -6
  28. data/lib/shoulda/matchers/active_record/have_readonly_attribute_matcher.rb +26 -2
  29. data/lib/shoulda/matchers/active_record/have_rich_text_matcher.rb +1 -1
  30. data/lib/shoulda/matchers/active_record/uniqueness/namespace.rb +19 -2
  31. data/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb +12 -22
  32. data/lib/shoulda/matchers/doublespeak/double.rb +4 -4
  33. data/lib/shoulda/matchers/doublespeak/method_call.rb +1 -1
  34. data/lib/shoulda/matchers/doublespeak/object_double.rb +3 -3
  35. data/lib/shoulda/matchers/independent/delegate_method_matcher.rb +2 -4
  36. data/lib/shoulda/matchers/integrations/configuration.rb +0 -2
  37. data/lib/shoulda/matchers/matcher_collection.rb +99 -0
  38. data/lib/shoulda/matchers/rails_shim.rb +4 -8
  39. data/lib/shoulda/matchers/routing.rb +1 -1
  40. data/lib/shoulda/matchers/util/word_wrap.rb +4 -4
  41. data/lib/shoulda/matchers/version.rb +1 -1
  42. data/lib/shoulda/matchers.rb +1 -0
  43. data/shoulda-matchers.gemspec +5 -3
  44. metadata +22 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 28aa81eb37b239f9f7715725027b774736992c82c2f6905b0d244a35f9db080d
4
- data.tar.gz: 2433121dc12d086b12c19f5313abcaf8c885cd03de594dd3bcaae0f9150c4173
3
+ metadata.gz: 223ff92a4826a7403e0f1016553453323108899c2d2f7029873ea9f99b68a1c0
4
+ data.tar.gz: 2014f3ded4620ce71736c035b8f73959a2a85c1adead8067692173fbca6907de
5
5
  SHA512:
6
- metadata.gz: c3dab42effbda8285e424feab81c543bb2e84d9764b513db8007a6b6bb0a2dd923fc76db8f253013dc701588dda5ca314fa3d7e2de9e15af84a8a84f9245670b
7
- data.tar.gz: 6902d75fa7b45c3adc8c97917e9fc1c610be1e99a95bb860ba5112793cdb7653189e4f303f8c879842576a679e3975a520d8ff98dc160ad2168b3fa0b105ecf5
6
+ metadata.gz: 3a23155ac210adef58ea71d3cead443b1277b0292972f1365c140b20c671de2f743c39516871ab0ce6ffe0e48b2298a86635533f9fb1f2838577320505b6f91f
7
+ data.tar.gz: 6999714a118645d1aff05c32ad2a7337111aa7c7501f42822a34ca28e31b5d7caa84d49138ecebf0c1dfad17e9f24fd25583e41b73f9e981c4b9ff1da673fd39
data/README.md CHANGED
@@ -63,7 +63,7 @@ Start by including `shoulda-matchers` in your Gemfile:
63
63
 
64
64
  ```ruby
65
65
  group :test do
66
- gem 'shoulda-matchers', '~> 6.0'
66
+ gem 'shoulda-matchers', '~> 8.0'
67
67
  end
68
68
  ```
69
69
 
@@ -125,7 +125,7 @@ Otherwise, add `shoulda-matchers` to your Gemfile:
125
125
 
126
126
  ```ruby
127
127
  group :test do
128
- gem 'shoulda-matchers', '~> 6.0'
128
+ gem 'shoulda-matchers', '~> 8.0'
129
129
  end
130
130
  ```
131
131
 
@@ -341,6 +341,7 @@ RSpec.describe MySpecialModel, type: :model do
341
341
  end
342
342
  ```
343
343
 
344
+ <a name="should-vs-is_expectedto"></a>
344
345
  ### `should` vs `is_expected.to`
345
346
 
346
347
  In this README and throughout the documentation, you'll notice that we use the
@@ -503,14 +504,18 @@ machine, understanding the codebase, and creating a good pull request.
503
504
 
504
505
  ## Compatibility
505
506
 
506
- Shoulda Matchers is tested and supported against Ruby 3.0+, Rails
507
- 6.1+, RSpec 3.x, and Minitest 5.x.
507
+ Shoulda Matchers is tested and supported against Ruby 3.3+, Rails
508
+ 7.2+, RSpec 3.x, and Minitest 5.x.
508
509
 
509
510
  - For Ruby < 2.4 and Rails < 4.1 compatibility, please use [v3.1.3][v3.1.3].
510
511
  - For Ruby < 3.0 and Rails < 6.1 compatibility, please use [v4.5.1][v4.5.1].
512
+ - For Rails < 7.1 compatibility, please use [v6.5.0][v6.5.0].
513
+ - For Ruby < 3.3 and Rails < 7.2 compatibility, please use [v7.0.1][v7.0.1].
511
514
 
512
515
  [v3.1.3]: https://github.com/thoughtbot/shoulda-matchers/tree/v3.1.3
513
516
  [v4.5.1]: https://github.com/thoughtbot/shoulda-matchers/tree/v4.5.1
517
+ [v6.5.0]: https://github.com/thoughtbot/shoulda-matchers/tree/v6.5.0
518
+ [v7.0.1]: https://github.com/thoughtbot/shoulda-matchers/tree/v7.0.1
514
519
 
515
520
  ## Versioning
516
521
 
@@ -559,5 +564,4 @@ We are [available for hire][hire].
559
564
  [community]: https://thoughtbot.com/community?utm_source=github
560
565
  [hire]: https://thoughtbot.com/hire-us?utm_source=github
561
566
 
562
-
563
567
  <!-- END /templates/footer.md -->
@@ -111,11 +111,7 @@ module Shoulda
111
111
  end
112
112
 
113
113
  def rendered_with_expected_layout?
114
- if @expected_layout.nil?
115
- true
116
- else
117
- rendered_layouts.include?(@expected_layout)
118
- end
114
+ @expected_layout.nil? || rendered_layouts.include?(@expected_layout)
119
115
  end
120
116
 
121
117
  def rendered_layouts
@@ -125,7 +125,7 @@ module Shoulda
125
125
  # @return [RouteMatcher]
126
126
  #
127
127
  def route(method, path, port: nil)
128
- RouteMatcher.new(self, method, path, port: port)
128
+ RouteMatcher.new(self, method, path, port:)
129
129
  end
130
130
 
131
131
  # @private
@@ -190,7 +190,7 @@ module Shoulda
190
190
  def route_recognized?
191
191
  context.send(
192
192
  :assert_routing,
193
- { method: method, path: path },
193
+ { method:, path: },
194
194
  params,
195
195
  )
196
196
  true
@@ -27,7 +27,7 @@ module Shoulda
27
27
 
28
28
  def extract_params_from_string
29
29
  controller, action = args[0].split('#')
30
- params = (args[1] || {}).merge!(controller: controller, action: action)
30
+ params = (args[1] || {}).merge!(controller:, action:)
31
31
  normalize_values(params)
32
32
  end
33
33
 
@@ -204,9 +204,9 @@ module Shoulda
204
204
  def attribute_changed_value_error
205
205
  AttributeChangedValueError.create(
206
206
  model: object.class,
207
- attribute_name: attribute_name,
208
- value_written: value_written,
209
- value_read: value_read,
207
+ attribute_name:,
208
+ value_written:,
209
+ value_read:,
210
210
  )
211
211
  end
212
212
 
@@ -217,7 +217,7 @@ module Shoulda
217
217
  def attribute_does_not_exist_error
218
218
  AttributeDoesNotExistError.create(
219
219
  model: object.class,
220
- attribute_name: attribute_name,
220
+ attribute_name:,
221
221
  value: value_written,
222
222
  )
223
223
  end
@@ -31,9 +31,9 @@ module Shoulda
31
31
  @_attribute_setter ||= AttributeSetter.new(
32
32
  matcher_name: :allow_value,
33
33
  object: instance,
34
- attribute_name: attribute_name,
35
- value: value,
36
- ignore_interference_by_writer: ignore_interference_by_writer,
34
+ attribute_name:,
35
+ value:,
36
+ ignore_interference_by_writer:,
37
37
  after_set_callback: after_setting_value_callback,
38
38
  )
39
39
  end
@@ -46,9 +46,9 @@ module Shoulda
46
46
  @_validator ||= Validator.new(
47
47
  instance,
48
48
  attribute_to_check_message_against,
49
- context: context,
49
+ context:,
50
50
  expects_strict: expects_strict?,
51
- expected_message: expected_message,
51
+ expected_message:,
52
52
  )
53
53
  end
54
54
 
@@ -458,7 +458,7 @@ module Shoulda
458
458
  message << '.'
459
459
  else
460
460
  message << " producing these validation errors:\n\n"
461
- message << validator.all_formatted_validation_error_messages
461
+ message << validator.formatted_validation_error_messages
462
462
  end
463
463
  end
464
464
 
@@ -672,8 +672,8 @@ pass, or do something else entirely.
672
672
 
673
673
  def default_attribute_message_values
674
674
  defaults = {
675
- model_name: model_name,
676
- instance: instance,
675
+ model_name:,
676
+ instance:,
677
677
  attribute: attribute_to_check_message_against,
678
678
  }
679
679
 
@@ -7,7 +7,7 @@ module Shoulda
7
7
  # @private
8
8
  class NonNullableBooleanError < Shoulda::Matchers::Error
9
9
  def self.create(attribute)
10
- super(attribute: attribute)
10
+ super(attribute:)
11
11
  end
12
12
 
13
13
  attr_accessor :attribute
@@ -24,7 +24,7 @@ Hence, this test will fail and there is no way to make it pass.
24
24
  # @private
25
25
  class CouldNotSetPasswordError < Shoulda::Matchers::Error
26
26
  def self.create(model)
27
- super(model: model)
27
+ super(model:)
28
28
  end
29
29
 
30
30
  attr_accessor :model
@@ -73,7 +73,7 @@ module Shoulda
73
73
 
74
74
  def failure_message_when_negated
75
75
  MESSAGES[:should_not_have_secure_password] %
76
- { subject: @subject.class, description: description }
76
+ { subject: @subject.class, description: }
77
77
  end
78
78
 
79
79
  protected
@@ -7,8 +7,10 @@ module Shoulda
7
7
  format_validation_errors(object.errors)
8
8
  end
9
9
 
10
- def format_validation_errors(errors)
10
+ def format_validation_errors(errors, attr = nil)
11
11
  list_items = errors.to_hash.keys.map do |attribute|
12
+ next if attr && attr.to_sym != attribute.to_sym
13
+
12
14
  messages = errors[attribute]
13
15
  "* #{attribute}: #{messages}"
14
16
  end
@@ -70,10 +70,33 @@ module Shoulda
70
70
  # with_message("there shall be peace on Earth")
71
71
  # end
72
72
  #
73
+ # #### Multiple attributes
74
+ #
75
+ # You can pass multiple attributes to assert that each one has the
76
+ # validation. Any qualifier chained on the matcher is applied to
77
+ # every attribute uniformly.
78
+ #
79
+ # class Robot
80
+ # include ActiveModel::Model
81
+ # attr_accessor :arms, :legs
82
+ #
83
+ # validates_absence_of :arms, :legs
84
+ # end
85
+ #
86
+ # # RSpec
87
+ # RSpec.describe Robot, type: :model do
88
+ # it { should validate_absence_of(:arms, :legs) }
89
+ # end
90
+ #
91
+ # # Minitest (Shoulda)
92
+ # class RobotTest < ActiveSupport::TestCase
93
+ # should validate_absence_of(:arms, :legs)
94
+ # end
95
+ #
73
96
  # @return [ValidateAbsenceOfMatcher}
74
97
  #
75
- def validate_absence_of(attr)
76
- ValidateAbsenceOfMatcher.new(attr)
98
+ def validate_absence_of(*attrs)
99
+ MatcherCollection.build(attrs) { |attr| ValidateAbsenceOfMatcher.new(attr) }
77
100
  end
78
101
 
79
102
  # @private
@@ -73,10 +73,33 @@ module Shoulda
73
73
  # with_message('You must accept the terms of service')
74
74
  # end
75
75
  #
76
+ # #### Multiple attributes
77
+ #
78
+ # You can pass multiple attributes to assert that each one has the
79
+ # validation. Any qualifier chained on the matcher is applied to
80
+ # every attribute uniformly.
81
+ #
82
+ # class User
83
+ # include ActiveModel::Model
84
+ # attr_accessor :terms, :privacy_policy
85
+ #
86
+ # validates_acceptance_of :terms, :privacy_policy
87
+ # end
88
+ #
89
+ # # RSpec
90
+ # RSpec.describe User, type: :model do
91
+ # it { should validate_acceptance_of(:terms, :privacy_policy) }
92
+ # end
93
+ #
94
+ # # Minitest (Shoulda)
95
+ # class UserTest < ActiveSupport::TestCase
96
+ # should validate_acceptance_of(:terms, :privacy_policy)
97
+ # end
98
+ #
76
99
  # @return [ValidateAcceptanceOfMatcher]
77
100
  #
78
- def validate_acceptance_of(attr)
79
- ValidateAcceptanceOfMatcher.new(attr)
101
+ def validate_acceptance_of(*attrs)
102
+ MatcherCollection.build(attrs) { |attr| ValidateAcceptanceOfMatcher.new(attr) }
80
103
  end
81
104
 
82
105
  # @private
@@ -272,10 +272,33 @@ module Shoulda
272
272
  # should validate_comparison_of(:age).is_greater_than(0).allow_nil
273
273
  # end
274
274
  #
275
+ # #### Multiple attributes
276
+ #
277
+ # You can pass multiple attributes to assert that each one has the
278
+ # validation. Any qualifier chained on the matcher is applied to
279
+ # every attribute uniformly.
280
+ #
281
+ # class Item
282
+ # include ActiveModel::Model
283
+ # attr_accessor :width, :height
284
+ #
285
+ # validates_comparison_of :width, :height, greater_than: 0
286
+ # end
287
+ #
288
+ # # RSpec
289
+ # RSpec.describe Item, type: :model do
290
+ # it { should validate_comparison_of(:width, :height).is_greater_than(0) }
291
+ # end
292
+ #
293
+ # # Minitest (Shoulda)
294
+ # class ItemTest < ActiveSupport::TestCase
295
+ # should validate_comparison_of(:width, :height).is_greater_than(0)
296
+ # end
297
+ #
275
298
  # @return [ValidateComparisonOfMatcher]
276
299
  #
277
- def validate_comparison_of(attr)
278
- ValidateComparisonOfMatcher.new(attr)
300
+ def validate_comparison_of(*attrs)
301
+ MatcherCollection.build(attrs) { |attr| ValidateComparisonOfMatcher.new(attr) }
279
302
  end
280
303
 
281
304
  # @private
@@ -378,6 +401,13 @@ module Shoulda
378
401
  end
379
402
  end
380
403
 
404
+ def failure_reason
405
+ raw_submatcher_failure_reason_for(
406
+ first_submatcher_that_fails_to_match,
407
+ :failure_message,
408
+ )
409
+ end
410
+
381
411
  def given_numeric_column?
382
412
  attribute_is_active_record_column? &&
383
413
  [:integer, :float, :decimal].include?(column_type)
@@ -485,21 +515,26 @@ module Shoulda
485
515
  submatcher,
486
516
  failure_message_method
487
517
  )
518
+ Shoulda::Matchers.word_wrap(
519
+ raw_submatcher_failure_reason_for(submatcher, failure_message_method),
520
+ indent: 2,
521
+ )
522
+ end
523
+
524
+ def raw_submatcher_failure_reason_for(submatcher, failure_message_method)
488
525
  failure_message = submatcher.public_send(failure_message_method)
489
526
  submatcher_description = submatcher.simple_description.
490
527
  sub(/\bvalidate that\b/, 'validates').
491
528
  sub(/\bdisallow\b/, 'disallows').
492
529
  sub(/\ballow\b/, 'allows')
493
- submatcher_message =
494
- if number_of_submatchers_for_failure_message > 1
495
- "In checking that #{model.name} #{submatcher_description}, " +
496
- failure_message[0].downcase +
497
- failure_message[1..]
498
- else
499
- failure_message
500
- end
501
530
 
502
- Shoulda::Matchers.word_wrap(submatcher_message, indent: 2)
531
+ if number_of_submatchers_for_failure_message > 1
532
+ "In checking that #{model.name} #{submatcher_description}, " +
533
+ failure_message[0].downcase +
534
+ failure_message[1..]
535
+ else
536
+ failure_message
537
+ end
503
538
  end
504
539
 
505
540
  def comparison_descriptions
@@ -70,10 +70,33 @@ module Shoulda
70
70
  # with_message('Please re-enter your password')
71
71
  # end
72
72
  #
73
+ # #### Multiple attributes
74
+ #
75
+ # You can pass multiple attributes to assert that each one has the
76
+ # validation. Any qualifier chained on the matcher is applied to
77
+ # every attribute uniformly.
78
+ #
79
+ # class User
80
+ # include ActiveModel::Model
81
+ # attr_accessor :password, :password_confirmation, :email, :email_confirmation
82
+ #
83
+ # validates_confirmation_of :password, :email
84
+ # end
85
+ #
86
+ # # RSpec
87
+ # RSpec.describe User, type: :model do
88
+ # it { should validate_confirmation_of(:password, :email) }
89
+ # end
90
+ #
91
+ # # Minitest (Shoulda)
92
+ # class UserTest < ActiveSupport::TestCase
93
+ # should validate_confirmation_of(:password, :email)
94
+ # end
95
+ #
73
96
  # @return [ValidateConfirmationOfMatcher]
74
97
  #
75
- def validate_confirmation_of(attr)
76
- ValidateConfirmationOfMatcher.new(attr)
98
+ def validate_confirmation_of(*attrs)
99
+ MatcherCollection.build(attrs) { |attr| ValidateConfirmationOfMatcher.new(attr) }
77
100
  end
78
101
 
79
102
  # @private
@@ -153,7 +176,7 @@ module Shoulda
153
176
  matcher.with_message(
154
177
  @expected_message,
155
178
  against: confirmation_attribute,
156
- values: { attribute: attribute },
179
+ values: { attribute: },
157
180
  )
158
181
  end
159
182
  end
@@ -112,10 +112,33 @@ module Shoulda
112
112
  # with_message('You chose a puny weapon')
113
113
  # end
114
114
  #
115
+ # #### Multiple attributes
116
+ #
117
+ # You can pass multiple attributes to assert that each one has the
118
+ # validation. Any qualifier chained on the matcher is applied to
119
+ # every attribute uniformly.
120
+ #
121
+ # class Article
122
+ # include ActiveModel::Model
123
+ # attr_accessor :slug, :handle
124
+ #
125
+ # validates_exclusion_of :slug, :handle, in: %w[admin root]
126
+ # end
127
+ #
128
+ # # RSpec
129
+ # RSpec.describe Article, type: :model do
130
+ # it { should validate_exclusion_of(:slug, :handle).in_array(%w[admin root]) }
131
+ # end
132
+ #
133
+ # # Minitest (Shoulda)
134
+ # class ArticleTest < ActiveSupport::TestCase
135
+ # should validate_exclusion_of(:slug, :handle).in_array(%w[admin root])
136
+ # end
137
+ #
115
138
  # @return [ValidateExclusionOfMatcher]
116
139
  #
117
- def validate_exclusion_of(attr)
118
- ValidateExclusionOfMatcher.new(attr)
140
+ def validate_exclusion_of(*attrs)
141
+ MatcherCollection.build(attrs) { |attr| ValidateExclusionOfMatcher.new(attr) }
119
142
  end
120
143
 
121
144
  # @private
@@ -263,10 +263,33 @@ module Shoulda
263
263
  # allow_blank
264
264
  # end
265
265
  #
266
+ # #### Multiple attributes
267
+ #
268
+ # You can pass multiple attributes to assert that each one has the
269
+ # validation. Any qualifier chained on the matcher is applied to
270
+ # every attribute uniformly.
271
+ #
272
+ # class Article
273
+ # include ActiveModel::Model
274
+ # attr_accessor :status, :category
275
+ #
276
+ # validates_inclusion_of :status, :category, in: %w[draft published]
277
+ # end
278
+ #
279
+ # # RSpec
280
+ # RSpec.describe Article, type: :model do
281
+ # it { should validate_inclusion_of(:status, :category).in_array(%w[draft published]) }
282
+ # end
283
+ #
284
+ # # Minitest (Shoulda)
285
+ # class ArticleTest < ActiveSupport::TestCase
286
+ # should validate_inclusion_of(:status, :category).in_array(%w[draft published])
287
+ # end
288
+ #
266
289
  # @return [ValidateInclusionOfMatcher]
267
290
  #
268
- def validate_inclusion_of(attr)
269
- ValidateInclusionOfMatcher.new(attr)
291
+ def validate_inclusion_of(*attrs)
292
+ MatcherCollection.build(attrs) { |attr| ValidateInclusionOfMatcher.new(attr) }
270
293
  end
271
294
 
272
295
  # @private
@@ -278,18 +301,6 @@ module Shoulda
278
301
  ARBITRARY_OUTSIDE_DATE = Date.jd(9999999)
279
302
  ARBITRARY_OUTSIDE_DATETIME = DateTime.jd(9999999)
280
303
  ARBITRARY_OUTSIDE_TIME = Time.at(9999999999)
281
- BOOLEAN_ALLOWS_BOOLEAN_MESSAGE = <<EOT.freeze
282
- You are using `validate_inclusion_of` to assert that a boolean column allows
283
- boolean values and disallows non-boolean ones. Be aware that it is not possible
284
- to fully test this, as boolean columns will automatically convert non-boolean
285
- values to boolean ones. Hence, you should consider removing this test.
286
- EOT
287
- BOOLEAN_ALLOWS_NIL_MESSAGE = <<EOT.freeze
288
- You are using `validate_inclusion_of` to assert that a boolean column allows nil.
289
- Be aware that it is not possible to fully test this, as anything other than
290
- true, false or nil will be converted to false. Hence, you should consider
291
- removing this test.
292
- EOT
293
304
 
294
305
  def initialize(attribute)
295
306
  super(attribute)
@@ -400,6 +411,24 @@ EOT
400
411
 
401
412
  private
402
413
 
414
+ def boolean_allows_boolean_message
415
+ <<-EOT.strip
416
+ You are using `validate_inclusion_of` to assert that the column '#{@subject.class.name}##{attribute}'
417
+ allows boolean values and disallows non-boolean ones. Be aware that it
418
+ is not possible to fully test this, as boolean columns will automatically
419
+ convert non-boolean values to boolean ones. Hence, you should consider
420
+ removing this test.
421
+ EOT
422
+ end
423
+
424
+ def boolean_allows_nil_message
425
+ <<-EOT.strip
426
+ You are using `validate_inclusion_of` to assert that the column '#{@subject.class.name}##{attribute}'
427
+ allows nil. Be aware that it is not possible to fully test this, as anything other than
428
+ true, false or nil will be converted to false. Hence, you should consider removing this test.
429
+ EOT
430
+ end
431
+
403
432
  def minimum_range_value
404
433
  @range.begin
405
434
  end
@@ -492,11 +521,11 @@ EOT
492
521
  if attribute_type == :boolean
493
522
  case @array
494
523
  when [false, true], [true, false]
495
- Shoulda::Matchers.warn BOOLEAN_ALLOWS_BOOLEAN_MESSAGE
524
+ Shoulda::Matchers.warn boolean_allows_boolean_message
496
525
  return true
497
526
  when [nil]
498
527
  if attribute_column.null
499
- Shoulda::Matchers.warn BOOLEAN_ALLOWS_NIL_MESSAGE
528
+ Shoulda::Matchers.warn boolean_allows_nil_message
500
529
  return true
501
530
  else
502
531
  raise NonNullableBooleanError.create(@attribute)
@@ -513,11 +542,11 @@ EOT
513
542
  if attribute_type == :boolean
514
543
  case @array
515
544
  when [false, true], [true, false]
516
- Shoulda::Matchers.warn BOOLEAN_ALLOWS_BOOLEAN_MESSAGE
545
+ Shoulda::Matchers.warn boolean_allows_boolean_message
517
546
  return true
518
547
  when [nil]
519
548
  if attribute_column.null
520
- Shoulda::Matchers.warn BOOLEAN_ALLOWS_NIL_MESSAGE
549
+ Shoulda::Matchers.warn boolean_allows_nil_message
521
550
  return true
522
551
  else
523
552
  raise NonNullableBooleanError.create(@attribute)
@@ -240,7 +240,6 @@ module Shoulda
240
240
  # should validate_length_of(:bio).is_at_least(15).allow_nil
241
241
  # end
242
242
  #
243
- # @return [ValidateLengthOfMatcher]
244
243
  #
245
244
  # ##### allow_blank
246
245
  #
@@ -286,8 +285,32 @@ module Shoulda
286
285
  # should validate_length_of(:arr).as_array.is_at_least(15)
287
286
  # end
288
287
  #
289
- def validate_length_of(attr)
290
- ValidateLengthOfMatcher.new(attr)
288
+ # #### Multiple attributes
289
+ #
290
+ # You can pass multiple attributes to assert that each one has the
291
+ # validation. Any qualifier chained on the matcher is applied to
292
+ # every attribute uniformly.
293
+ #
294
+ # class User
295
+ # include ActiveModel::Model
296
+ # attr_accessor :first_name, :last_name
297
+ #
298
+ # validates_length_of :first_name, :last_name, minimum: 2
299
+ # end
300
+ #
301
+ # # RSpec
302
+ # RSpec.describe User, type: :model do
303
+ # it { should validate_length_of(:first_name, :last_name).is_at_least(2) }
304
+ # end
305
+ #
306
+ # # Minitest (Shoulda)
307
+ # class UserTest < ActiveSupport::TestCase
308
+ # should validate_length_of(:first_name, :last_name).is_at_least(2)
309
+ # end
310
+ #
311
+ # @return [ValidateLengthOfMatcher]
312
+ def validate_length_of(*attrs)
313
+ MatcherCollection.build(attrs) { |attr| ValidateLengthOfMatcher.new(attr) }
291
314
  end
292
315
 
293
316
  # @private