shoulda-matchers 2.2.0 → 2.3.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.
- data/.travis.yml +3 -0
- data/Gemfile.lock +1 -1
- data/NEWS.md +25 -1
- data/README.md +1 -2
- data/gemfiles/3.0.gemfile.lock +1 -1
- data/gemfiles/3.1.gemfile.lock +1 -1
- data/gemfiles/3.2.gemfile.lock +1 -1
- data/lib/shoulda/matchers/action_controller.rb +1 -0
- data/lib/shoulda/matchers/action_controller/rescue_from_matcher.rb +81 -0
- data/lib/shoulda/matchers/active_model/comparison_matcher.rb +1 -1
- data/lib/shoulda/matchers/active_model/disallow_value_matcher.rb +5 -0
- data/lib/shoulda/matchers/active_model/ensure_inclusion_of_matcher.rb +15 -1
- data/lib/shoulda/matchers/active_model/validate_acceptance_of_matcher.rb +1 -1
- data/lib/shoulda/matchers/active_model/validate_numericality_of_matcher.rb +1 -1
- data/lib/shoulda/matchers/active_model/validate_uniqueness_of_matcher.rb +2 -8
- data/lib/shoulda/matchers/active_model/validation_matcher.rb +2 -0
- data/lib/shoulda/matchers/active_record.rb +5 -0
- data/lib/shoulda/matchers/active_record/association_matcher.rb +90 -113
- data/lib/shoulda/matchers/active_record/association_matchers/counter_cache_matcher.rb +35 -0
- data/lib/shoulda/matchers/active_record/association_matchers/dependent_matcher.rb +35 -0
- data/lib/shoulda/matchers/active_record/association_matchers/model_reflector.rb +37 -0
- data/lib/shoulda/matchers/active_record/association_matchers/order_matcher.rb +35 -0
- data/lib/shoulda/matchers/active_record/association_matchers/through_matcher.rb +57 -0
- data/lib/shoulda/matchers/version.rb +1 -1
- data/spec/shoulda/matchers/action_controller/rescue_from_matcher_spec.rb +63 -0
- data/spec/shoulda/matchers/active_model/allow_value_matcher_spec.rb +1 -1
- data/spec/shoulda/matchers/active_model/comparison_matcher_spec.rb +5 -0
- data/spec/shoulda/matchers/active_model/disallow_value_matcher_spec.rb +18 -0
- data/spec/shoulda/matchers/active_model/ensure_inclusion_of_matcher_spec.rb +10 -0
- data/spec/shoulda/matchers/active_model/ensure_length_of_matcher_spec.rb +3 -3
- data/spec/shoulda/matchers/active_model/validate_numericality_of_matcher_spec.rb +13 -0
- data/spec/shoulda/matchers/active_model/validate_presence_of_matcher_spec.rb +14 -0
- data/spec/shoulda/matchers/active_model/validate_uniqueness_of_matcher_spec.rb +49 -1
- data/spec/shoulda/matchers/active_record/association_matcher_spec.rb +39 -4
- metadata +11 -3
@@ -0,0 +1,35 @@
|
|
1
|
+
module Shoulda # :nodoc:
|
2
|
+
module Matchers
|
3
|
+
module ActiveRecord # :nodoc:
|
4
|
+
module AssociationMatchers
|
5
|
+
class CounterCacheMatcher
|
6
|
+
attr_accessor :missing_option
|
7
|
+
|
8
|
+
def initialize(counter_cache, name)
|
9
|
+
@counter_cache = counter_cache
|
10
|
+
@name = name
|
11
|
+
@missing_option = ''
|
12
|
+
end
|
13
|
+
|
14
|
+
def description
|
15
|
+
"counter_cache => #{counter_cache}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def matches?(subject)
|
19
|
+
subject = ModelReflector.new(subject, name)
|
20
|
+
|
21
|
+
if subject.option_set_properly?(counter_cache, :counter_cache)
|
22
|
+
true
|
23
|
+
else
|
24
|
+
self.missing_option = "#{name} should have #{description}"
|
25
|
+
false
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
attr_accessor :counter_cache, :name
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Shoulda # :nodoc:
|
2
|
+
module Matchers
|
3
|
+
module ActiveRecord # :nodoc:
|
4
|
+
module AssociationMatchers
|
5
|
+
class DependentMatcher
|
6
|
+
attr_accessor :missing_option
|
7
|
+
|
8
|
+
def initialize(dependent, name)
|
9
|
+
@dependent = dependent
|
10
|
+
@name = name
|
11
|
+
@missing_option = ''
|
12
|
+
end
|
13
|
+
|
14
|
+
def description
|
15
|
+
"dependent => #{dependent}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def matches?(subject)
|
19
|
+
subject = ModelReflector.new(subject, name)
|
20
|
+
|
21
|
+
if dependent.nil? || subject.option_set_properly?(dependent, :dependent)
|
22
|
+
true
|
23
|
+
else
|
24
|
+
self.missing_option = "#{name} should have #{dependent} dependency"
|
25
|
+
false
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
attr_accessor :dependent, :name
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Shoulda # :nodoc:
|
2
|
+
module Matchers
|
3
|
+
module ActiveRecord # :nodoc:
|
4
|
+
module AssociationMatchers
|
5
|
+
class ModelReflector
|
6
|
+
def initialize(subject, name)
|
7
|
+
@subject = subject
|
8
|
+
@name = name
|
9
|
+
end
|
10
|
+
|
11
|
+
def reflection
|
12
|
+
@reflection ||= reflect_on_association(name)
|
13
|
+
end
|
14
|
+
|
15
|
+
def reflect_on_association(name)
|
16
|
+
model_class.reflect_on_association(name)
|
17
|
+
end
|
18
|
+
|
19
|
+
def model_class
|
20
|
+
subject.class
|
21
|
+
end
|
22
|
+
|
23
|
+
def option_string(key)
|
24
|
+
reflection.options[key].to_s
|
25
|
+
end
|
26
|
+
|
27
|
+
def option_set_properly?(option, option_key)
|
28
|
+
option.to_s == option_string(option_key)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
attr_reader :subject, :name
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Shoulda # :nodoc:
|
2
|
+
module Matchers
|
3
|
+
module ActiveRecord # :nodoc:
|
4
|
+
module AssociationMatchers
|
5
|
+
class OrderMatcher
|
6
|
+
attr_accessor :missing_option
|
7
|
+
|
8
|
+
def initialize(order, name)
|
9
|
+
@order = order
|
10
|
+
@name = name
|
11
|
+
@missing_option = ''
|
12
|
+
end
|
13
|
+
|
14
|
+
def description
|
15
|
+
"order => #{order}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def matches?(subject)
|
19
|
+
subject = ModelReflector.new(subject, name)
|
20
|
+
|
21
|
+
if subject.option_set_properly?(order, :order)
|
22
|
+
true
|
23
|
+
else
|
24
|
+
self.missing_option = "#{name} should be ordered by #{order}"
|
25
|
+
false
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
attr_accessor :order, :name
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Shoulda # :nodoc:
|
2
|
+
module Matchers
|
3
|
+
module ActiveRecord # :nodoc:
|
4
|
+
module AssociationMatchers
|
5
|
+
class ThroughMatcher
|
6
|
+
attr_accessor :missing_option
|
7
|
+
|
8
|
+
def initialize(through, name)
|
9
|
+
@through = through
|
10
|
+
@name = name
|
11
|
+
@missing_option = ''
|
12
|
+
end
|
13
|
+
|
14
|
+
def description
|
15
|
+
"through #{through}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def matches?(subject)
|
19
|
+
self.subject = ModelReflector.new(subject, name)
|
20
|
+
through.nil? || association_set_properly?
|
21
|
+
end
|
22
|
+
|
23
|
+
def association_set_properly?
|
24
|
+
through_association_exists? && through_association_correct?
|
25
|
+
end
|
26
|
+
|
27
|
+
def through_association_exists?
|
28
|
+
if through_reflection.present?
|
29
|
+
true
|
30
|
+
else
|
31
|
+
self.missing_option = "#{name} does not have any relationship to #{through}"
|
32
|
+
false
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def through_reflection
|
37
|
+
@through_reflection ||= subject.reflect_on_association(through)
|
38
|
+
end
|
39
|
+
|
40
|
+
def through_association_correct?
|
41
|
+
if subject.option_set_properly?(through, :through)
|
42
|
+
true
|
43
|
+
else
|
44
|
+
self.missing_option =
|
45
|
+
"Expected #{name} to have #{name} through #{through}, " +
|
46
|
+
"but got it through #{subject.option_string(:through)}"
|
47
|
+
false
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
attr_accessor :through, :name, :subject
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Shoulda::Matchers::ActionController::RescueFromMatcher do
|
4
|
+
context 'a controller that rescues from RuntimeError' do
|
5
|
+
it "asserts controller is setup with rescue_from" do
|
6
|
+
controller_with_rescue_from.should rescue_from RuntimeError
|
7
|
+
end
|
8
|
+
|
9
|
+
context 'with a handler method' do
|
10
|
+
it "asserts rescue_from was set up with handler method" do
|
11
|
+
controller_with_rescue_from_and_method.should rescue_from(RuntimeError).with(:error_method)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "asserts rescue_from was not set up with incorrect handler method" do
|
15
|
+
controller_with_rescue_from_and_method.should_not rescue_from(RuntimeError).with(:other_method)
|
16
|
+
end
|
17
|
+
|
18
|
+
it "asserts the controller responds to the handler method" do
|
19
|
+
matcher = rescue_from(RuntimeError).with(:error_method)
|
20
|
+
matcher.matches?(controller_with_rescue_from_and_invalid_method).should be_false
|
21
|
+
matcher.failure_message_for_should.should =~ /does not respond to/
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'without a handler method' do
|
26
|
+
it "the handler method is not included in the description" do
|
27
|
+
matcher = rescue_from(RuntimeError)
|
28
|
+
matcher.matches?(controller_with_rescue_from).should be_true
|
29
|
+
matcher.description.should_not =~ /with #/
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
context 'a controller that does not rescue from RuntimeError' do
|
35
|
+
it "asserts controller is not setup with rescue_from" do
|
36
|
+
matcher = rescue_from RuntimeError
|
37
|
+
define_controller("RandomController").should_not matcher
|
38
|
+
matcher.failure_message_for_should_not.should =~ /Did not expect \w+ to rescue from/
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def controller_with_rescue_from
|
43
|
+
define_controller "RescueRuntimeError" do
|
44
|
+
rescue_from(RuntimeError) {}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def controller_with_rescue_from_and_invalid_method
|
49
|
+
define_controller "RescueRuntimeErrorWithMethod" do
|
50
|
+
rescue_from RuntimeError, with: :error_method
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def controller_with_rescue_from_and_method
|
55
|
+
controller = controller_with_rescue_from_and_invalid_method
|
56
|
+
class << controller
|
57
|
+
def error_method
|
58
|
+
true
|
59
|
+
end
|
60
|
+
end
|
61
|
+
controller
|
62
|
+
end
|
63
|
+
end
|
@@ -110,7 +110,7 @@ describe Shoulda::Matchers::ActiveModel::AllowValueMatcher do
|
|
110
110
|
matcher = described_class.new('foo').for(:attr)
|
111
111
|
matcher.description
|
112
112
|
|
113
|
-
expect { matcher.matches?(model) }.not_to raise_error
|
113
|
+
expect { matcher.matches?(model) }.not_to raise_error
|
114
114
|
end
|
115
115
|
end
|
116
116
|
|
@@ -21,6 +21,11 @@ describe Shoulda::Matchers::ActiveModel::ComparisonMatcher do
|
|
21
21
|
it { instance_without_validations.should_not matcher.is_less_than_or_equal_to(2) }
|
22
22
|
end
|
23
23
|
|
24
|
+
context 'is_equal_to' do
|
25
|
+
it { instance_with_validations(:equal_to => 0).should matcher.is_equal_to(0) }
|
26
|
+
it { instance_without_validations.should_not matcher.is_equal_to(0) }
|
27
|
+
end
|
28
|
+
|
24
29
|
def instance_with_validations(options = {})
|
25
30
|
define_model :example, :attr => :string do
|
26
31
|
validates_numericality_of :attr, options
|
@@ -15,6 +15,24 @@ describe Shoulda::Matchers::ActiveModel::DisallowValueMatcher do
|
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
18
|
+
context "an attribute with a context-dependent validation" do
|
19
|
+
context "without the validation context" do
|
20
|
+
it "does not match" do
|
21
|
+
validating_format(:with => /abc/, :on => :customisable).should_not matcher("xyz").for(:attr)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context "with the validation context" do
|
26
|
+
it "disallows a bad value" do
|
27
|
+
validating_format(:with => /abc/, :on => :customisable).should matcher("xyz").for(:attr).on(:customisable)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "does not match a good value" do
|
31
|
+
validating_format(:with => /abc/, :on => :customisable).should_not matcher("abcde").for(:attr).on(:customisable)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
18
36
|
context 'an attribute with a format validation and a custom message' do
|
19
37
|
it 'does not match if the value and message are both correct' do
|
20
38
|
validating_format(:with => /abc/, :message => 'good message').
|
@@ -8,6 +8,16 @@ describe Shoulda::Matchers::ActiveModel::EnsureInclusionOfMatcher do
|
|
8
8
|
end
|
9
9
|
end
|
10
10
|
|
11
|
+
context 'with an integer column' do
|
12
|
+
it 'can verify a zero in the array' do
|
13
|
+
model = define_model(:example, :attr => :integer) do
|
14
|
+
validates_inclusion_of :attr, :in => [0, 1, 2]
|
15
|
+
end.new
|
16
|
+
|
17
|
+
model.should ensure_inclusion_of(:attr).in_array([0,1,2])
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
11
21
|
context 'with true/false values' do
|
12
22
|
it 'can verify outside values to ensure the negative case' do
|
13
23
|
define_model(:example, :attr => :string).new.
|
@@ -118,7 +118,7 @@ describe Shoulda::Matchers::ActiveModel::EnsureLengthOfMatcher do
|
|
118
118
|
it "does not raise an exception" do
|
119
119
|
expect {
|
120
120
|
validating_length(:maximum => 4).should ensure_length_of(:attr).is_at_most(4)
|
121
|
-
}.to_not raise_exception
|
121
|
+
}.to_not raise_exception
|
122
122
|
end
|
123
123
|
end
|
124
124
|
|
@@ -132,7 +132,7 @@ describe Shoulda::Matchers::ActiveModel::EnsureLengthOfMatcher do
|
|
132
132
|
it "does not raise an exception" do
|
133
133
|
expect {
|
134
134
|
validating_length(:minimum => 4).should ensure_length_of(:attr).is_at_least(4)
|
135
|
-
}.to_not raise_exception
|
135
|
+
}.to_not raise_exception
|
136
136
|
end
|
137
137
|
end
|
138
138
|
|
@@ -146,7 +146,7 @@ describe Shoulda::Matchers::ActiveModel::EnsureLengthOfMatcher do
|
|
146
146
|
it "does not raise an exception" do
|
147
147
|
expect {
|
148
148
|
validating_length(:is => 4).should ensure_length_of(:attr).is_equal_to(4)
|
149
|
-
}.to_not raise_exception
|
149
|
+
}.to_not raise_exception
|
150
150
|
end
|
151
151
|
end
|
152
152
|
end
|
@@ -121,6 +121,19 @@ describe Shoulda::Matchers::ActiveModel::ValidateNumericalityOfMatcher do
|
|
121
121
|
end
|
122
122
|
end
|
123
123
|
|
124
|
+
context 'when the subject is stubbed' do
|
125
|
+
it 'retains stubs on submatchers' do
|
126
|
+
subject = define_model :example, :attr => :string do
|
127
|
+
validates_numericality_of :attr, :odd => true
|
128
|
+
before_validation :set_attr!
|
129
|
+
def set_attr!; self.attr = 5 end
|
130
|
+
end.new
|
131
|
+
|
132
|
+
subject.stubs(:set_attr!)
|
133
|
+
subject.should matcher.odd
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
124
137
|
def validating_numericality(options = {})
|
125
138
|
define_model :example, :attr => :string do
|
126
139
|
validates_numericality_of :attr, options
|
@@ -120,6 +120,20 @@ describe Shoulda::Matchers::ActiveModel::ValidatePresenceOfMatcher do
|
|
120
120
|
end
|
121
121
|
end
|
122
122
|
|
123
|
+
context "an attribute with a context-dependent validation" do
|
124
|
+
context "without the validation context" do
|
125
|
+
it "does not match" do
|
126
|
+
validating_presence(:on => :customisable).should_not matcher
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
context "with the validation context" do
|
131
|
+
it "matches" do
|
132
|
+
validating_presence(:on => :customisable).should matcher.on(:customisable)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
123
137
|
def matcher
|
124
138
|
validate_presence_of(:attr)
|
125
139
|
end
|
@@ -131,6 +131,13 @@ describe Shoulda::Matchers::ActiveModel::ValidateUniquenessOfMatcher do
|
|
131
131
|
should_not matcher.scoped_to(:scope1, :scope2, :other)
|
132
132
|
end
|
133
133
|
end
|
134
|
+
|
135
|
+
context 'when too broad of a scope is specified' do
|
136
|
+
it 'rejects' do
|
137
|
+
validating_scoped_uniqueness([:scope1, :scope2], :date, :scope1 => Date.today, :scope2 => Date.today).
|
138
|
+
should_not matcher.scoped_to(:scope1)
|
139
|
+
end
|
140
|
+
end
|
134
141
|
end
|
135
142
|
|
136
143
|
context 'when the scoped attribute is a datetime' do
|
@@ -160,12 +167,53 @@ describe Shoulda::Matchers::ActiveModel::ValidateUniquenessOfMatcher do
|
|
160
167
|
end
|
161
168
|
end
|
162
169
|
|
170
|
+
context 'when too broad of a scope is specified' do
|
171
|
+
it 'rejects' do
|
172
|
+
validating_scoped_uniqueness([:scope1, :scope2], :datetime, :scope1 => DateTime.now, :scope2 => DateTime.now).
|
173
|
+
should_not matcher.scoped_to(:scope1)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
context 'when the scoped attribute is a uuid' do
|
179
|
+
it 'accepts' do
|
180
|
+
validating_scoped_uniqueness([:scope1], :uuid, :scope1 => SecureRandom.uuid).
|
181
|
+
should matcher.scoped_to(:scope1)
|
182
|
+
end
|
183
|
+
|
163
184
|
context 'with an existing record that conflicts with scope.next' do
|
164
185
|
it 'accepts' do
|
165
|
-
validating_scoped_uniqueness_with_conflicting_next(:scope1, :scope1 =>
|
186
|
+
validating_scoped_uniqueness_with_conflicting_next(:scope1, :uuid, :scope1 => SecureRandom.uuid).
|
166
187
|
should matcher.scoped_to(:scope1)
|
167
188
|
end
|
168
189
|
end
|
190
|
+
|
191
|
+
context 'with a nil value' do
|
192
|
+
it 'accepts' do
|
193
|
+
validating_scoped_uniqueness([:scope1], :uuid, :scope1 => nil).
|
194
|
+
should matcher.scoped_to(:scope1)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
context 'when too narrow of a scope is specified' do
|
199
|
+
it 'rejects' do
|
200
|
+
record = validating_scoped_uniqueness([:scope1, :scope2], :uuid,
|
201
|
+
:scope1 => SecureRandom.uuid,
|
202
|
+
:scope2 => SecureRandom.uuid
|
203
|
+
)
|
204
|
+
record.should_not matcher.scoped_to(:scope1, :scope2, :other)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
context 'when too broad of a scope is specified' do
|
209
|
+
it 'rejects' do
|
210
|
+
record = validating_scoped_uniqueness([:scope1, :scope2], :uuid,
|
211
|
+
:scope1 => SecureRandom.uuid,
|
212
|
+
:scope2 => SecureRandom.uuid
|
213
|
+
)
|
214
|
+
record.should_not matcher.scoped_to(:scope1)
|
215
|
+
end
|
216
|
+
end
|
169
217
|
end
|
170
218
|
|
171
219
|
def create_existing_record(attributes = {})
|