shoulda-activemodel 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (26) hide show
  1. data/lib/shoulda/active_model.rb +16 -0
  2. data/lib/shoulda/active_model/assertions.rb +61 -0
  3. data/lib/shoulda/active_model/helpers.rb +31 -0
  4. data/lib/shoulda/active_model/macros.rb +268 -0
  5. data/lib/shoulda/active_model/matchers.rb +30 -0
  6. data/lib/shoulda/active_model/matchers/allow_value_matcher.rb +102 -0
  7. data/lib/shoulda/active_model/matchers/ensure_inclusion_of_matcher.rb +87 -0
  8. data/lib/shoulda/active_model/matchers/ensure_length_of_matcher.rb +141 -0
  9. data/lib/shoulda/active_model/matchers/validate_acceptance_of_matcher.rb +41 -0
  10. data/lib/shoulda/active_model/matchers/validate_format_of_matcher.rb +67 -0
  11. data/lib/shoulda/active_model/matchers/validate_numericality_of_matcher.rb +39 -0
  12. data/lib/shoulda/active_model/matchers/validate_presence_of_matcher.rb +60 -0
  13. data/lib/shoulda/active_model/matchers/validate_uniqueness_of_matcher.rb +148 -0
  14. data/lib/shoulda/active_model/matchers/validation_matcher.rb +57 -0
  15. data/test/fail_macros.rb +39 -0
  16. data/test/matchers/active_model/ensure_inclusion_of_matcher_test.rb +80 -0
  17. data/test/matchers/active_model/ensure_length_of_matcher_test.rb +158 -0
  18. data/test/matchers/active_model/validate_acceptance_of_matcher_test.rb +44 -0
  19. data/test/matchers/active_model/validate_format_of_matcher_test.rb +39 -0
  20. data/test/matchers/active_model/validate_numericality_of_matcher_test.rb +52 -0
  21. data/test/matchers/active_model/validate_presence_of_matcher_test.rb +86 -0
  22. data/test/matchers/active_model/validate_uniqueness_of_matcher_test.rb +147 -0
  23. data/test/model_builder.rb +106 -0
  24. data/test/rspec_test.rb +207 -0
  25. data/test/test_helper.rb +20 -0
  26. metadata +106 -0
@@ -0,0 +1,16 @@
1
+ require 'shoulda'
2
+ require 'shoulda/active_model/helpers'
3
+ require 'shoulda/active_model/matchers'
4
+ require 'shoulda/active_model/assertions'
5
+ require 'shoulda/active_model/macros'
6
+
7
+ module Test # :nodoc: all
8
+ module Unit
9
+ class TestCase
10
+ include Shoulda::ActiveModel::Helpers
11
+ include Shoulda::ActiveModel::Matchers
12
+ include Shoulda::ActiveModel::Assertions
13
+ extend Shoulda::ActiveModel::Macros
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,61 @@
1
+ module Shoulda # :nodoc:
2
+ module ActiveModel # :nodoc:
3
+ module Assertions
4
+ # Asserts that the given object is valid
5
+ #
6
+ # assert_valid User.new(params)
7
+ def assert_valid(obj)
8
+ assert obj.valid?, "Errors: #{pretty_error_messages obj}"
9
+ end
10
+
11
+ # Asserts that an Active Model model validates with the passed
12
+ # <tt>value</tt> by making sure the <tt>error_message_to_avoid</tt> is not
13
+ # contained within the list of errors for that attribute.
14
+ #
15
+ # assert_good_value(User.new, :email, "user@example.com")
16
+ # assert_good_value(User.new, :ssn, "123456789", /length/)
17
+ #
18
+ # If a class is passed as the first argument, a new object will be
19
+ # instantiated before the assertion. If an instance variable exists with
20
+ # the same name as the class (underscored), that object will be used
21
+ # instead.
22
+ #
23
+ # assert_good_value(User, :email, "user@example.com")
24
+ #
25
+ # product = Product.new(:tangible => false)
26
+ # assert_good_value(product, :price, "0")
27
+ def assert_good_value(object_or_klass, attribute, value, error_message_to_avoid = nil)
28
+ object = get_instance_of(object_or_klass)
29
+ matcher = allow_value(value).
30
+ for(attribute).
31
+ with_message(error_message_to_avoid)
32
+ assert_accepts(matcher, object)
33
+ end
34
+
35
+ # Asserts that an Active Model model invalidates the passed
36
+ # <tt>value</tt> by making sure the <tt>error_message_to_expect</tt> is
37
+ # contained within the list of errors for that attribute.
38
+ #
39
+ # assert_bad_value(User.new, :email, "invalid")
40
+ # assert_bad_value(User.new, :ssn, "123", /length/)
41
+ #
42
+ # If a class is passed as the first argument, a new object will be
43
+ # instantiated before the assertion. If an instance variable exists with
44
+ # the same name as the class (underscored), that object will be used
45
+ # instead.
46
+ #
47
+ # assert_bad_value(User, :email, "invalid")
48
+ #
49
+ # product = Product.new(:tangible => true)
50
+ # assert_bad_value(product, :price, "0")
51
+ def assert_bad_value(object_or_klass, attribute, value,
52
+ error_message_to_expect = nil)
53
+ object = get_instance_of(object_or_klass)
54
+ matcher = allow_value(value).
55
+ for(attribute).
56
+ with_message(error_message_to_expect)
57
+ assert_rejects(matcher, object)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,31 @@
1
+ module Shoulda # :nodoc:
2
+ module ActiveModel # :nodoc:
3
+ module Helpers
4
+ def pretty_error_messages(obj) # :nodoc:
5
+ obj.errors.map do |a, m|
6
+ msg = "#{a} #{m}"
7
+ msg << " (#{obj.send(a).inspect})" unless a.to_sym == :base
8
+ end
9
+ end
10
+
11
+ # Helper method that determines the default error message used by Active
12
+ # Record. Works for both existing Rails 2.1 and Rails 2.2 with the newly
13
+ # introduced I18n module used for localization.
14
+ #
15
+ # default_error_message(:blank)
16
+ # default_error_message(:too_short, :count => 5)
17
+ # default_error_message(:too_long, :count => 60)
18
+ def default_error_message(key, values = {})
19
+ if Object.const_defined?(:I18n) # Rails >= 2.2
20
+ message = I18n.translate("activerecord.errors.messages.#{key}", values, :default => '')
21
+ if message.blank? # Rails 3 where the namespace has moved
22
+ message = I18n.translate("errors.messages.#{key}", values)
23
+ end
24
+ message
25
+ else # Rails <= 2.1.x
26
+ ::ActiveRecord::Errors.default_error_messages[key] % values[:count]
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,268 @@
1
+ module Shoulda # :nodoc:
2
+ module ActiveModel # :nodoc:
3
+ # = Macro test helpers for your active model models
4
+ #
5
+ # These helpers will test most of the validations and associations for your ActiveModel models.
6
+ #
7
+ # class UserTest < Test::Unit::TestCase
8
+ # should_validate_presence_of :name, :phone_number
9
+ # should_not_allow_values_for :phone_number, "abcd", "1234"
10
+ # should_allow_values_for :phone_number, "(123) 456-7890"
11
+ # end
12
+ #
13
+ # For all of these helpers, the last parameter may be a hash of options.
14
+ #
15
+ module Macros
16
+ include Matchers
17
+
18
+ # Ensures that the model cannot be saved if one of the attributes listed is not present.
19
+ #
20
+ # Options:
21
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
22
+ # Regexp or string. Default = <tt>I18n.translate('activemodel.errors.messages.blank')</tt>
23
+ #
24
+ # Example:
25
+ # should_validate_presence_of :name, :phone_number
26
+ #
27
+ def should_validate_presence_of(*attributes)
28
+ message = get_options!(attributes, :message)
29
+
30
+ attributes.each do |attribute|
31
+ matcher = validate_presence_of(attribute).with_message(message)
32
+ should matcher.description do
33
+ assert_accepts(matcher, subject)
34
+ end
35
+ end
36
+ end
37
+
38
+ # Ensures that the model cannot be saved if one of the attributes listed is not unique.
39
+ # Requires an existing model
40
+ #
41
+ # Options:
42
+
43
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
44
+ # Regexp or string. Default = <tt>I18n.translate('activemodel.errors.messages.taken')</tt>
45
+ # * <tt>:scoped_to</tt> - field(s) to scope the uniqueness to.
46
+ # * <tt>:case_sensitive</tt> - whether or not uniqueness is defined by an
47
+ # exact match. Ignored by non-text attributes. Default = <tt>true</tt>
48
+ #
49
+ # Examples:
50
+ # should_validate_uniqueness_of :keyword, :username
51
+ # should_validate_uniqueness_of :name, :message => "O NOES! SOMEONE STOELED YER NAME!"
52
+ # should_validate_uniqueness_of :email, :scoped_to => :name
53
+ # should_validate_uniqueness_of :address, :scoped_to => [:first_name, :last_name]
54
+ # should_validate_uniqueness_of :email, :case_sensitive => false
55
+ #
56
+ def should_validate_uniqueness_of(*attributes)
57
+ message, scope, case_sensitive = get_options!(attributes, :message, :scoped_to, :case_sensitive)
58
+ scope = [*scope].compact
59
+ case_sensitive = true if case_sensitive.nil?
60
+
61
+ attributes.each do |attribute|
62
+ matcher = validate_uniqueness_of(attribute).
63
+ with_message(message).scoped_to(scope)
64
+ matcher = matcher.case_insensitive unless case_sensitive
65
+ should matcher.description do
66
+ assert_accepts(matcher, subject)
67
+ end
68
+ end
69
+ end
70
+
71
+ # Ensures that the attribute cannot be set to the given values
72
+ #
73
+ # Options:
74
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
75
+ # Regexp or string. If omitted, the test will pass if there is ANY error in
76
+ # <tt>errors.on(:attribute)</tt>.
77
+ #
78
+ # Example:
79
+ # should_not_allow_values_for :isbn, "bad 1", "bad 2"
80
+ #
81
+ def should_not_allow_values_for(attribute, *bad_values)
82
+ message = get_options!(bad_values, :message)
83
+ bad_values.each do |value|
84
+ matcher = allow_value(value).for(attribute).with_message(message)
85
+ should "not #{matcher.description}" do
86
+ assert_rejects matcher, subject
87
+ end
88
+ end
89
+ end
90
+
91
+ # Ensures that the attribute can be set to the given values.
92
+ #
93
+ # Example:
94
+ # should_allow_values_for :isbn, "isbn 1 2345 6789 0", "ISBN 1-2345-6789-0"
95
+ #
96
+ def should_allow_values_for(attribute, *good_values)
97
+ get_options!(good_values)
98
+ good_values.each do |value|
99
+ matcher = allow_value(value).for(attribute)
100
+ should matcher.description do
101
+ assert_accepts matcher, subject
102
+ end
103
+ end
104
+ end
105
+
106
+ # Ensures that the length of the attribute is in the given range
107
+ #
108
+ # Options:
109
+ # * <tt>:short_message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
110
+ # Regexp or string. Default = <tt>I18n.translate('activemodel.errors.messages.too_short') % range.first</tt>
111
+ # * <tt>:long_message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
112
+ # Regexp or string. Default = <tt>I18n.translate('activemodel.errors.messages.too_long') % range.last</tt>
113
+ #
114
+ # Example:
115
+ # should_ensure_length_in_range :password, (6..20)
116
+ #
117
+ def should_ensure_length_in_range(attribute, range, opts = {})
118
+ short_message, long_message = get_options!([opts],
119
+ :short_message,
120
+ :long_message)
121
+ matcher = ensure_length_of(attribute).
122
+ is_at_least(range.first).
123
+ with_short_message(short_message).
124
+ is_at_most(range.last).
125
+ with_long_message(long_message)
126
+
127
+ should matcher.description do
128
+ assert_accepts matcher, subject
129
+ end
130
+ end
131
+
132
+ # Ensures that the length of the attribute is at least a certain length
133
+ #
134
+ # Options:
135
+ # * <tt>:short_message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
136
+ # Regexp or string. Default = <tt>I18n.translate('activemodel.errors.messages.too_short') % min_length</tt>
137
+ #
138
+ # Example:
139
+ # should_ensure_length_at_least :name, 3
140
+ #
141
+ def should_ensure_length_at_least(attribute, min_length, opts = {})
142
+ short_message = get_options!([opts], :short_message)
143
+
144
+ matcher = ensure_length_of(attribute).
145
+ is_at_least(min_length).
146
+ with_short_message(short_message)
147
+
148
+ should matcher.description do
149
+ assert_accepts matcher, subject
150
+ end
151
+ end
152
+
153
+ # Ensures that the length of the attribute is exactly a certain length
154
+ #
155
+ # Options:
156
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
157
+ # Regexp or string. Default = <tt>I18n.translate('activemodel.errors.messages.wrong_length') % length</tt>
158
+ #
159
+ # Example:
160
+ # should_ensure_length_is :ssn, 9
161
+ #
162
+ def should_ensure_length_is(attribute, length, opts = {})
163
+ message = get_options!([opts], :message)
164
+ matcher = ensure_length_of(attribute).
165
+ is_equal_to(length).
166
+ with_message(message)
167
+
168
+ should matcher.description do
169
+ assert_accepts matcher, subject
170
+ end
171
+ end
172
+
173
+ # Ensure that the attribute is in the range specified
174
+ #
175
+ # Options:
176
+ # * <tt>:low_message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
177
+ # Regexp or string. Default = <tt>I18n.translate('activemodel.errors.messages.inclusion')</tt>
178
+ # * <tt>:high_message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
179
+ # Regexp or string. Default = <tt>I18n.translate('activemodel.errors.messages.inclusion')</tt>
180
+ #
181
+ # Example:
182
+ # should_ensure_value_in_range :age, (0..100)
183
+ #
184
+ def should_ensure_value_in_range(attribute, range, opts = {})
185
+ message, low_message, high_message = get_options!([opts],
186
+ :message,
187
+ :low_message,
188
+ :high_message)
189
+ matcher = ensure_inclusion_of(attribute).
190
+ in_range(range).
191
+ with_message(message).
192
+ with_low_message(low_message).
193
+ with_high_message(high_message)
194
+ should matcher.description do
195
+ assert_accepts matcher, subject
196
+ end
197
+ end
198
+
199
+ # Ensure that the attribute is numeric
200
+ #
201
+ # Options:
202
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
203
+ # Regexp or string. Default = <tt>I18n.translate('activemodel.errors.messages.not_a_number')</tt>
204
+ #
205
+ # Example:
206
+ # should_validate_numericality_of :age
207
+ #
208
+ def should_validate_numericality_of(*attributes)
209
+ message = get_options!(attributes, :message)
210
+ attributes.each do |attribute|
211
+ matcher = validate_numericality_of(attribute).
212
+ with_message(message)
213
+ should matcher.description do
214
+ assert_accepts matcher, subject
215
+ end
216
+ end
217
+ end
218
+
219
+ # Ensure that the given class methods are defined on the model.
220
+ #
221
+ # should_have_class_methods :find, :destroy
222
+ #
223
+ def should_have_class_methods(*methods)
224
+ get_options!(methods)
225
+ klass = described_type
226
+ methods.each do |method|
227
+ should "respond to class method ##{method}" do
228
+ assert_respond_to klass, method, "#{klass.name} does not have class method #{method}"
229
+ end
230
+ end
231
+ end
232
+
233
+ # Ensure that the given instance methods are defined on the model.
234
+ #
235
+ # should_have_instance_methods :email, :name, :name=
236
+ #
237
+ def should_have_instance_methods(*methods)
238
+ get_options!(methods)
239
+ klass = described_type
240
+ methods.each do |method|
241
+ should "respond to instance method ##{method}" do
242
+ assert_respond_to klass.new, method, "#{klass.name} does not have instance method #{method}"
243
+ end
244
+ end
245
+ end
246
+
247
+ # Ensures that the model cannot be saved if one of the attributes listed is not accepted.
248
+ #
249
+ # Options:
250
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
251
+ # Regexp or string. Default = <tt>I18n.translate('activemodel.errors.messages.accepted')</tt>
252
+ #
253
+ # Example:
254
+ # should_validate_acceptance_of :eula
255
+ #
256
+ def should_validate_acceptance_of(*attributes)
257
+ message = get_options!(attributes, :message)
258
+
259
+ attributes.each do |attribute|
260
+ matcher = validate_acceptance_of(attribute).with_message(message)
261
+ should matcher.description do
262
+ assert_accepts matcher, subject
263
+ end
264
+ end
265
+ end
266
+ end
267
+ end
268
+ end
@@ -0,0 +1,30 @@
1
+ require 'shoulda/active_model/matchers/validation_matcher'
2
+ require 'shoulda/active_model/matchers/allow_value_matcher'
3
+ require 'shoulda/active_model/matchers/ensure_length_of_matcher'
4
+ require 'shoulda/active_model/matchers/ensure_inclusion_of_matcher'
5
+ require 'shoulda/active_model/matchers/validate_presence_of_matcher'
6
+ require 'shoulda/active_model/matchers/validate_format_of_matcher'
7
+ require 'shoulda/active_model/matchers/validate_uniqueness_of_matcher'
8
+ require 'shoulda/active_model/matchers/validate_acceptance_of_matcher'
9
+ require 'shoulda/active_model/matchers/validate_numericality_of_matcher'
10
+
11
+ module Shoulda # :nodoc:
12
+ module ActiveModel # :nodoc:
13
+ # = Matchers for your ActiveModel models
14
+ #
15
+ # These matchers will test most of the validations and associations for your
16
+ # ActiveModel models.
17
+ #
18
+ # describe User do
19
+ # it { should validate_presence_of(:name) }
20
+ # it { should validate_presence_of(:phone_number) }
21
+ # %w(abcd 1234).each do |value|
22
+ # it { should_not allow_value(value).for(:phone_number) }
23
+ # end
24
+ # it { should allow_value("(123) 456-7890").for(:phone_number) }
25
+ # end
26
+ #
27
+ module Matchers
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,102 @@
1
+ module Shoulda # :nodoc:
2
+ module ActiveModel # :nodoc:
3
+ module Matchers
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 = @instance.errors[@attribute]
63
+ @errors = [@errors] unless @errors.is_a?(Array)
64
+ @expected_message ? (errors_match_regexp? || errors_match_string?) : (@errors != [nil])
65
+ end
66
+
67
+ def errors_match_regexp?
68
+ if Regexp === @expected_message
69
+ @matched_error = @errors.detect { |e| e =~ @expected_message }
70
+ !@matched_error.nil?
71
+ else
72
+ false
73
+ end
74
+ end
75
+
76
+ def errors_match_string?
77
+ if @errors.include?(@expected_message)
78
+ @matched_error = @expected_message
79
+ true
80
+ else
81
+ false
82
+ end
83
+ end
84
+
85
+ def expectation
86
+ "errors " <<
87
+ (@expected_message ? "to include #{@expected_message.inspect} " : "") <<
88
+ "when #{@attribute} is set to #{@value.inspect}"
89
+ end
90
+
91
+ def error_description
92
+ if @instance.errors.empty?
93
+ "no errors"
94
+ else
95
+ "errors: #{pretty_error_messages(@instance)}"
96
+ end
97
+ end
98
+ end
99
+
100
+ end
101
+ end
102
+ end