shoulda-matchers 2.2.0 → 2.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 = {})
|