shoulda-activemodel 0.0.2

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 (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