shoulda-matchers 1.0.0.beta1

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 (40) hide show
  1. data/CONTRIBUTION_GUIDELINES.rdoc +10 -0
  2. data/Gemfile +10 -0
  3. data/Gemfile.lock +116 -0
  4. data/MIT-LICENSE +22 -0
  5. data/README.rdoc +70 -0
  6. data/Rakefile +50 -0
  7. data/lib/shoulda-matchers.rb +8 -0
  8. data/lib/shoulda/matchers/action_controller.rb +38 -0
  9. data/lib/shoulda/matchers/action_controller/assign_to_matcher.rb +114 -0
  10. data/lib/shoulda/matchers/action_controller/filter_param_matcher.rb +50 -0
  11. data/lib/shoulda/matchers/action_controller/redirect_to_matcher.rb +62 -0
  12. data/lib/shoulda/matchers/action_controller/render_template_matcher.rb +54 -0
  13. data/lib/shoulda/matchers/action_controller/render_with_layout_matcher.rb +99 -0
  14. data/lib/shoulda/matchers/action_controller/respond_with_content_type_matcher.rb +74 -0
  15. data/lib/shoulda/matchers/action_controller/respond_with_matcher.rb +85 -0
  16. data/lib/shoulda/matchers/action_controller/route_matcher.rb +93 -0
  17. data/lib/shoulda/matchers/action_controller/set_session_matcher.rb +98 -0
  18. data/lib/shoulda/matchers/action_controller/set_the_flash_matcher.rb +94 -0
  19. data/lib/shoulda/matchers/action_mailer.rb +22 -0
  20. data/lib/shoulda/matchers/action_mailer/have_sent_email.rb +115 -0
  21. data/lib/shoulda/matchers/active_record.rb +42 -0
  22. data/lib/shoulda/matchers/active_record/allow_mass_assignment_of_matcher.rb +83 -0
  23. data/lib/shoulda/matchers/active_record/allow_value_matcher.rb +110 -0
  24. data/lib/shoulda/matchers/active_record/association_matcher.rb +226 -0
  25. data/lib/shoulda/matchers/active_record/ensure_inclusion_of_matcher.rb +87 -0
  26. data/lib/shoulda/matchers/active_record/ensure_length_of_matcher.rb +141 -0
  27. data/lib/shoulda/matchers/active_record/have_db_column_matcher.rb +169 -0
  28. data/lib/shoulda/matchers/active_record/have_db_index_matcher.rb +112 -0
  29. data/lib/shoulda/matchers/active_record/have_readonly_attribute_matcher.rb +59 -0
  30. data/lib/shoulda/matchers/active_record/helpers.rb +34 -0
  31. data/lib/shoulda/matchers/active_record/validate_acceptance_of_matcher.rb +41 -0
  32. data/lib/shoulda/matchers/active_record/validate_format_of_matcher.rb +65 -0
  33. data/lib/shoulda/matchers/active_record/validate_numericality_of_matcher.rb +39 -0
  34. data/lib/shoulda/matchers/active_record/validate_presence_of_matcher.rb +60 -0
  35. data/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb +148 -0
  36. data/lib/shoulda/matchers/active_record/validation_matcher.rb +56 -0
  37. data/lib/shoulda/matchers/integrations/rspec.rb +23 -0
  38. data/lib/shoulda/matchers/integrations/test_unit.rb +41 -0
  39. data/lib/shoulda/matchers/version.rb +5 -0
  40. metadata +113 -0
@@ -0,0 +1,110 @@
1
+ module Shoulda # :nodoc:
2
+ module Matchers
3
+ module ActiveRecord # :nodoc:
4
+
5
+ # Ensures that the attribute can be set to the given value.
6
+ #
7
+ # Options:
8
+ # * <tt>with_message</tt> - value the test expects to find in
9
+ # <tt>errors.on(:attribute)</tt>. Regexp or string. If omitted,
10
+ # the test looks for any errors in <tt>errors.on(:attribute)</tt>.
11
+ #
12
+ # Example:
13
+ # it { should_not allow_value('bad').for(:isbn) }
14
+ # it { should allow_value("isbn 1 2345 6789 0").for(:isbn) }
15
+ #
16
+ def allow_value(value)
17
+ AllowValueMatcher.new(value)
18
+ end
19
+
20
+ class AllowValueMatcher # :nodoc:
21
+ include Helpers
22
+
23
+ def initialize(value)
24
+ @value = value
25
+ end
26
+
27
+ def for(attribute)
28
+ @attribute = attribute
29
+ self
30
+ end
31
+
32
+ def with_message(message)
33
+ @expected_message = message if message
34
+ self
35
+ end
36
+
37
+ def matches?(instance)
38
+ @instance = instance
39
+ if Symbol === @expected_message
40
+ @expected_message = default_error_message(@expected_message)
41
+ end
42
+ @instance.send("#{@attribute}=", @value)
43
+ !errors_match?
44
+ end
45
+
46
+ def failure_message
47
+ "Did not expect #{expectation}, got error: #{@matched_error}"
48
+ end
49
+
50
+ def negative_failure_message
51
+ "Expected #{expectation}, got #{error_description}"
52
+ end
53
+
54
+ def description
55
+ "allow #{@attribute} to be set to #{@value.inspect}"
56
+ end
57
+
58
+ private
59
+
60
+ def errors_match?
61
+ @instance.valid?
62
+ @errors = errors_for_attribute(@instance, @attribute)
63
+ @errors = [@errors] unless @errors.is_a?(Array)
64
+ @expected_message ? (errors_match_regexp? || errors_match_string?) : (@errors.compact.any?)
65
+ end
66
+
67
+ def errors_for_attribute(instance, attribute)
68
+ if instance.errors.respond_to?(:[])
69
+ instance.errors[attribute]
70
+ else
71
+ instance.errors.on(attribute)
72
+ end
73
+ end
74
+
75
+ def errors_match_regexp?
76
+ if Regexp === @expected_message
77
+ @matched_error = @errors.detect { |e| e =~ @expected_message }
78
+ !@matched_error.nil?
79
+ else
80
+ false
81
+ end
82
+ end
83
+
84
+ def errors_match_string?
85
+ if @errors.include?(@expected_message)
86
+ @matched_error = @expected_message
87
+ true
88
+ else
89
+ false
90
+ end
91
+ end
92
+
93
+ def expectation
94
+ "errors " <<
95
+ (@expected_message ? "to include #{@expected_message.inspect} " : "") <<
96
+ "when #{@attribute} is set to #{@value.inspect}"
97
+ end
98
+
99
+ def error_description
100
+ if @instance.errors.empty?
101
+ "no errors"
102
+ else
103
+ "errors: #{pretty_error_messages(@instance)}"
104
+ end
105
+ end
106
+ end
107
+
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,226 @@
1
+ module Shoulda # :nodoc:
2
+ module Matchers
3
+ module ActiveRecord # :nodoc:
4
+
5
+ # Ensure that the belongs_to relationship exists.
6
+ #
7
+ # it { should belong_to(:parent) }
8
+ #
9
+ def belong_to(name)
10
+ AssociationMatcher.new(:belongs_to, name)
11
+ end
12
+
13
+ # Ensures that the has_many relationship exists. Will also test that the
14
+ # associated table has the required columns. Works with polymorphic
15
+ # associations.
16
+ #
17
+ # Options:
18
+ # * <tt>through</tt> - association name for <tt>has_many :through</tt>
19
+ # * <tt>dependent</tt> - tests that the association makes use of the
20
+ # dependent option.
21
+ #
22
+ # Example:
23
+ # it { should have_many(:friends) }
24
+ # it { should have_many(:enemies).through(:friends) }
25
+ # it { should have_many(:enemies).dependent(:destroy) }
26
+ #
27
+ def have_many(name)
28
+ AssociationMatcher.new(:has_many, name)
29
+ end
30
+
31
+ # Ensure that the has_one relationship exists. Will also test that the
32
+ # associated table has the required columns. Works with polymorphic
33
+ # associations.
34
+ #
35
+ # Options:
36
+ # * <tt>:dependent</tt> - tests that the association makes use of the
37
+ # dependent option.
38
+ #
39
+ # Example:
40
+ # it { should have_one(:god) } # unless hindu
41
+ #
42
+ def have_one(name)
43
+ AssociationMatcher.new(:has_one, name)
44
+ end
45
+
46
+ # Ensures that the has_and_belongs_to_many relationship exists, and that
47
+ # the join table is in place.
48
+ #
49
+ # it { should have_and_belong_to_many(:posts) }
50
+ #
51
+ def have_and_belong_to_many(name)
52
+ AssociationMatcher.new(:has_and_belongs_to_many, name)
53
+ end
54
+
55
+ class AssociationMatcher # :nodoc:
56
+ def initialize(macro, name)
57
+ @macro = macro
58
+ @name = name
59
+ end
60
+
61
+ def through(through)
62
+ @through = through
63
+ self
64
+ end
65
+
66
+ def dependent(dependent)
67
+ @dependent = dependent
68
+ self
69
+ end
70
+
71
+ def matches?(subject)
72
+ @subject = subject
73
+ association_exists? &&
74
+ macro_correct? &&
75
+ foreign_key_exists? &&
76
+ through_association_valid? &&
77
+ dependent_correct? &&
78
+ join_table_exists?
79
+ end
80
+
81
+ def failure_message
82
+ "Expected #{expectation} (#{@missing})"
83
+ end
84
+
85
+ def negative_failure_message
86
+ "Did not expect #{expectation}"
87
+ end
88
+
89
+ def description
90
+ description = "#{macro_description} #{@name}"
91
+ description += " through #{@through}" if @through
92
+ description += " dependent => #{@dependent}" if @dependent
93
+ description
94
+ end
95
+
96
+ protected
97
+
98
+ def association_exists?
99
+ if reflection.nil?
100
+ @missing = "no association called #{@name}"
101
+ false
102
+ else
103
+ true
104
+ end
105
+ end
106
+
107
+ def macro_correct?
108
+ if reflection.macro == @macro
109
+ true
110
+ else
111
+ @missing = "actual association type was #{reflection.macro}"
112
+ false
113
+ end
114
+ end
115
+
116
+ def foreign_key_exists?
117
+ !(belongs_foreign_key_missing? || has_foreign_key_missing?)
118
+ end
119
+
120
+ def belongs_foreign_key_missing?
121
+ @macro == :belongs_to && !class_has_foreign_key?(model_class)
122
+ end
123
+
124
+ def has_foreign_key_missing?
125
+ [:has_many, :has_one].include?(@macro) &&
126
+ !through? &&
127
+ !class_has_foreign_key?(associated_class)
128
+ end
129
+
130
+ def through_association_valid?
131
+ @through.nil? || (through_association_exists? && through_association_correct?)
132
+ end
133
+
134
+ def through_association_exists?
135
+ if through_reflection.nil?
136
+ @missing = "#{model_class.name} does not have any relationship to #{@through}"
137
+ false
138
+ else
139
+ true
140
+ end
141
+ end
142
+
143
+ def through_association_correct?
144
+ if @through == reflection.options[:through]
145
+ true
146
+ else
147
+ @missing = "Expected #{model_class.name} to have #{@name} through #{@through}, " <<
148
+ "but got it through #{reflection.options[:through]}"
149
+ false
150
+ end
151
+ end
152
+
153
+ def dependent_correct?
154
+ if @dependent.nil? || @dependent.to_s == reflection.options[:dependent].to_s
155
+ true
156
+ else
157
+ @missing = "#{@name} should have #{@dependent} dependency"
158
+ false
159
+ end
160
+ end
161
+
162
+ def join_table_exists?
163
+ if @macro != :has_and_belongs_to_many ||
164
+ ::ActiveRecord::Base.connection.tables.include?(join_table.to_s)
165
+ true
166
+ else
167
+ @missing = "join table #{join_table} doesn't exist"
168
+ false
169
+ end
170
+ end
171
+
172
+ def class_has_foreign_key?(klass)
173
+ if klass.column_names.include?(foreign_key.to_s)
174
+ true
175
+ else
176
+ @missing = "#{klass} does not have a #{foreign_key} foreign key."
177
+ false
178
+ end
179
+ end
180
+
181
+ def model_class
182
+ @subject.class
183
+ end
184
+
185
+ def join_table
186
+ reflection.options[:join_table]
187
+ end
188
+
189
+ def associated_class
190
+ reflection.klass
191
+ end
192
+
193
+ def foreign_key
194
+ reflection.primary_key_name
195
+ end
196
+
197
+ def through?
198
+ reflection.options[:through]
199
+ end
200
+
201
+ def reflection
202
+ @reflection ||= model_class.reflect_on_association(@name)
203
+ end
204
+
205
+ def through_reflection
206
+ @through_reflection ||= model_class.reflect_on_association(@through)
207
+ end
208
+
209
+ def expectation
210
+ "#{model_class.name} to have a #{@macro} association called #{@name}"
211
+ end
212
+
213
+ def macro_description
214
+ case @macro.to_s
215
+ when 'belongs_to' then 'belong to'
216
+ when 'has_many' then 'have many'
217
+ when 'has_one' then 'have one'
218
+ when 'has_and_belongs_to_many' then
219
+ 'have and belong to many'
220
+ end
221
+ end
222
+ end
223
+
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,87 @@
1
+ module Shoulda # :nodoc:
2
+ module Matchers
3
+ module ActiveRecord # :nodoc:
4
+
5
+ # Ensure that the attribute's value is in the range specified
6
+ #
7
+ # Options:
8
+ # * <tt>in_range</tt> - the range of allowed values for this attribute
9
+ # * <tt>with_low_message</tt> - value the test expects to find in
10
+ # <tt>errors.on(:attribute)</tt>. Regexp or string. Defaults to the
11
+ # translation for :inclusion.
12
+ # * <tt>with_high_message</tt> - value the test expects to find in
13
+ # <tt>errors.on(:attribute)</tt>. Regexp or string. Defaults to the
14
+ # translation for :inclusion.
15
+ #
16
+ # Example:
17
+ # it { should ensure_inclusion_of(:age).in_range(0..100) }
18
+ #
19
+ def ensure_inclusion_of(attr)
20
+ EnsureInclusionOfMatcher.new(attr)
21
+ end
22
+
23
+ class EnsureInclusionOfMatcher < ValidationMatcher # :nodoc:
24
+
25
+ def in_range(range)
26
+ @range = range
27
+ @minimum = range.first
28
+ @maximum = range.last
29
+ self
30
+ end
31
+
32
+ def with_message(message)
33
+ if message
34
+ @low_message = message
35
+ @high_message = message
36
+ end
37
+ self
38
+ end
39
+
40
+ def with_low_message(message)
41
+ @low_message = message if message
42
+ self
43
+ end
44
+
45
+ def with_high_message(message)
46
+ @high_message = message if message
47
+ self
48
+ end
49
+
50
+ def description
51
+ "ensure inclusion of #{@attribute} in #{@range.inspect}"
52
+ end
53
+
54
+ def matches?(subject)
55
+ super(subject)
56
+
57
+ @low_message ||= :inclusion
58
+ @high_message ||= :inclusion
59
+
60
+ disallows_lower_value &&
61
+ allows_minimum_value &&
62
+ disallows_higher_value &&
63
+ allows_maximum_value
64
+ end
65
+
66
+ private
67
+
68
+ def disallows_lower_value
69
+ @minimum == 0 || disallows_value_of(@minimum - 1, @low_message)
70
+ end
71
+
72
+ def disallows_higher_value
73
+ disallows_value_of(@maximum + 1, @high_message)
74
+ end
75
+
76
+ def allows_minimum_value
77
+ allows_value_of(@minimum, @low_message)
78
+ end
79
+
80
+ def allows_maximum_value
81
+ allows_value_of(@maximum, @high_message)
82
+ end
83
+ end
84
+
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,141 @@
1
+ module Shoulda # :nodoc:
2
+ module Matchers
3
+ module ActiveRecord # :nodoc:
4
+
5
+ # Ensures that the length of the attribute is validated.
6
+ #
7
+ # Options:
8
+ # * <tt>is_at_least</tt> - minimum length of this attribute
9
+ # * <tt>is_at_most</tt> - maximum length of this attribute
10
+ # * <tt>is_equal_to</tt> - exact requred length of this attribute
11
+ # * <tt>with_short_message</tt> - value the test expects to find in
12
+ # <tt>errors.on(:attribute)</tt>. Regexp or string. Defaults to the
13
+ # translation for :too_short.
14
+ # * <tt>with_long_message</tt> - value the test expects to find in
15
+ # <tt>errors.on(:attribute)</tt>. Regexp or string. Defaults to the
16
+ # translation for :too_long.
17
+ # * <tt>with_message</tt> - value the test expects to find in
18
+ # <tt>errors.on(:attribute)</tt>. Regexp or string. Defaults to the
19
+ # translation for :wrong_length. Used in conjunction with
20
+ # <tt>is_equal_to</tt>.
21
+ #
22
+ # Examples:
23
+ # it { should ensure_length_of(:password).
24
+ # is_at_least(6).
25
+ # is_at_most(20) }
26
+ # it { should ensure_length_of(:name).
27
+ # is_at_least(3).
28
+ # with_short_message(/not long enough/) }
29
+ # it { should ensure_length_of(:ssn).
30
+ # is_equal_to(9).
31
+ # with_message(/is invalid/) }
32
+ def ensure_length_of(attr)
33
+ EnsureLengthOfMatcher.new(attr)
34
+ end
35
+
36
+ class EnsureLengthOfMatcher < ValidationMatcher # :nodoc:
37
+ include Helpers
38
+
39
+ def is_at_least(length)
40
+ @minimum = length
41
+ @short_message ||= :too_short
42
+ self
43
+ end
44
+
45
+ def is_at_most(length)
46
+ @maximum = length
47
+ @long_message ||= :too_long
48
+ self
49
+ end
50
+
51
+ def is_equal_to(length)
52
+ @minimum = length
53
+ @maximum = length
54
+ @short_message ||= :wrong_length
55
+ self
56
+ end
57
+
58
+ def with_short_message(message)
59
+ @short_message = message if message
60
+ self
61
+ end
62
+ alias_method :with_message, :with_short_message
63
+
64
+ def with_long_message(message)
65
+ @long_message = message if message
66
+ self
67
+ end
68
+
69
+ def description
70
+ description = "ensure #{@attribute} has a length "
71
+ if @minimum && @maximum
72
+ if @minimum == @maximum
73
+ description << "of exactly #{@minimum}"
74
+ else
75
+ description << "between #{@minimum} and #{@maximum}"
76
+ end
77
+ else
78
+ description << "of at least #{@minimum}" if @minimum
79
+ description << "of at most #{@maximum}" if @maximum
80
+ end
81
+ description
82
+ end
83
+
84
+ def matches?(subject)
85
+ super(subject)
86
+ translate_messages!
87
+ disallows_lower_length &&
88
+ allows_minimum_length &&
89
+ ((@minimum == @maximum) ||
90
+ (disallows_higher_length &&
91
+ allows_maximum_length))
92
+ end
93
+
94
+ private
95
+
96
+ def translate_messages!
97
+ if Symbol === @short_message
98
+ @short_message = default_error_message(@short_message,
99
+ :count => @minimum)
100
+ end
101
+
102
+ if Symbol === @long_message
103
+ @long_message = default_error_message(@long_message,
104
+ :count => @maximum)
105
+ end
106
+ end
107
+
108
+ def disallows_lower_length
109
+ @minimum == 0 ||
110
+ @minimum.nil? ||
111
+ disallows_length_of(@minimum - 1, @short_message)
112
+ end
113
+
114
+ def disallows_higher_length
115
+ @maximum.nil? || disallows_length_of(@maximum + 1, @long_message)
116
+ end
117
+
118
+ def allows_minimum_length
119
+ allows_length_of(@minimum, @short_message)
120
+ end
121
+
122
+ def allows_maximum_length
123
+ allows_length_of(@maximum, @long_message)
124
+ end
125
+
126
+ def allows_length_of(length, message)
127
+ length.nil? || allows_value_of(string_of_length(length), message)
128
+ end
129
+
130
+ def disallows_length_of(length, message)
131
+ length.nil? || disallows_value_of(string_of_length(length), message)
132
+ end
133
+
134
+ def string_of_length(length)
135
+ 'x' * length
136
+ end
137
+ end
138
+
139
+ end
140
+ end
141
+ end