francois-shoulda 2.0.5.1

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 (104) hide show
  1. data/CONTRIBUTION_GUIDELINES.rdoc +12 -0
  2. data/MIT-LICENSE +22 -0
  3. data/README.rdoc +119 -0
  4. data/Rakefile +72 -0
  5. data/bin/convert_to_should_syntax +42 -0
  6. data/lib/shoulda.rb +17 -0
  7. data/lib/shoulda/action_mailer.rb +10 -0
  8. data/lib/shoulda/action_mailer/assertions.rb +39 -0
  9. data/lib/shoulda/active_record.rb +12 -0
  10. data/lib/shoulda/active_record/assertions.rb +96 -0
  11. data/lib/shoulda/active_record/macros.rb +752 -0
  12. data/lib/shoulda/assertions.rb +49 -0
  13. data/lib/shoulda/context.rb +306 -0
  14. data/lib/shoulda/controller.rb +30 -0
  15. data/lib/shoulda/controller/formats/html.rb +201 -0
  16. data/lib/shoulda/controller/formats/xml.rb +170 -0
  17. data/lib/shoulda/controller/helpers.rb +64 -0
  18. data/lib/shoulda/controller/macros.rb +316 -0
  19. data/lib/shoulda/controller/resource_options.rb +236 -0
  20. data/lib/shoulda/helpers.rb +10 -0
  21. data/lib/shoulda/macros.rb +74 -0
  22. data/lib/shoulda/private_helpers.rb +22 -0
  23. data/lib/shoulda/proc_extensions.rb +14 -0
  24. data/lib/shoulda/rails.rb +19 -0
  25. data/lib/shoulda/tasks.rb +3 -0
  26. data/lib/shoulda/tasks/list_tests.rake +24 -0
  27. data/lib/shoulda/tasks/yaml_to_shoulda.rake +28 -0
  28. data/rails/init.rb +1 -0
  29. data/test/README +36 -0
  30. data/test/fail_macros.rb +34 -0
  31. data/test/fixtures/addresses.yml +3 -0
  32. data/test/fixtures/friendships.yml +0 -0
  33. data/test/fixtures/posts.yml +5 -0
  34. data/test/fixtures/products.yml +0 -0
  35. data/test/fixtures/taggings.yml +0 -0
  36. data/test/fixtures/tags.yml +11 -0
  37. data/test/fixtures/users.yml +6 -0
  38. data/test/functional/posts_controller_test.rb +105 -0
  39. data/test/functional/users_controller_test.rb +38 -0
  40. data/test/other/context_test.rb +145 -0
  41. data/test/other/convert_to_should_syntax_test.rb +63 -0
  42. data/test/other/helpers_test.rb +183 -0
  43. data/test/other/private_helpers_test.rb +34 -0
  44. data/test/other/should_test.rb +266 -0
  45. data/test/rails_root/app/controllers/application.rb +25 -0
  46. data/test/rails_root/app/controllers/posts_controller.rb +85 -0
  47. data/test/rails_root/app/controllers/users_controller.rb +84 -0
  48. data/test/rails_root/app/helpers/application_helper.rb +3 -0
  49. data/test/rails_root/app/helpers/posts_helper.rb +2 -0
  50. data/test/rails_root/app/helpers/users_helper.rb +2 -0
  51. data/test/rails_root/app/models/address.rb +7 -0
  52. data/test/rails_root/app/models/dog.rb +5 -0
  53. data/test/rails_root/app/models/flea.rb +3 -0
  54. data/test/rails_root/app/models/friendship.rb +4 -0
  55. data/test/rails_root/app/models/post.rb +12 -0
  56. data/test/rails_root/app/models/product.rb +12 -0
  57. data/test/rails_root/app/models/tag.rb +8 -0
  58. data/test/rails_root/app/models/tagging.rb +4 -0
  59. data/test/rails_root/app/models/user.rb +28 -0
  60. data/test/rails_root/app/views/layouts/posts.rhtml +17 -0
  61. data/test/rails_root/app/views/layouts/users.rhtml +17 -0
  62. data/test/rails_root/app/views/layouts/wide.html.erb +1 -0
  63. data/test/rails_root/app/views/posts/edit.rhtml +27 -0
  64. data/test/rails_root/app/views/posts/index.rhtml +25 -0
  65. data/test/rails_root/app/views/posts/new.rhtml +26 -0
  66. data/test/rails_root/app/views/posts/show.rhtml +18 -0
  67. data/test/rails_root/app/views/users/edit.rhtml +22 -0
  68. data/test/rails_root/app/views/users/index.rhtml +22 -0
  69. data/test/rails_root/app/views/users/new.rhtml +21 -0
  70. data/test/rails_root/app/views/users/show.rhtml +13 -0
  71. data/test/rails_root/config/boot.rb +109 -0
  72. data/test/rails_root/config/database.yml +4 -0
  73. data/test/rails_root/config/environment.rb +14 -0
  74. data/test/rails_root/config/environments/sqlite3.rb +0 -0
  75. data/test/rails_root/config/initializers/new_rails_defaults.rb +15 -0
  76. data/test/rails_root/config/initializers/shoulda.rb +8 -0
  77. data/test/rails_root/config/routes.rb +6 -0
  78. data/test/rails_root/db/migrate/001_create_users.rb +18 -0
  79. data/test/rails_root/db/migrate/002_create_posts.rb +13 -0
  80. data/test/rails_root/db/migrate/003_create_taggings.rb +12 -0
  81. data/test/rails_root/db/migrate/004_create_tags.rb +11 -0
  82. data/test/rails_root/db/migrate/005_create_dogs.rb +12 -0
  83. data/test/rails_root/db/migrate/006_create_addresses.rb +14 -0
  84. data/test/rails_root/db/migrate/007_create_fleas.rb +11 -0
  85. data/test/rails_root/db/migrate/008_create_dogs_fleas.rb +12 -0
  86. data/test/rails_root/db/migrate/009_create_products.rb +17 -0
  87. data/test/rails_root/db/migrate/010_create_friendships.rb +14 -0
  88. data/test/rails_root/db/schema.rb +0 -0
  89. data/test/rails_root/public/404.html +30 -0
  90. data/test/rails_root/public/422.html +30 -0
  91. data/test/rails_root/public/500.html +30 -0
  92. data/test/rails_root/script/console +3 -0
  93. data/test/rails_root/script/generate +3 -0
  94. data/test/test_helper.rb +33 -0
  95. data/test/unit/address_test.rb +32 -0
  96. data/test/unit/dog_test.rb +7 -0
  97. data/test/unit/flea_test.rb +6 -0
  98. data/test/unit/friendship_test.rb +6 -0
  99. data/test/unit/post_test.rb +15 -0
  100. data/test/unit/product_test.rb +27 -0
  101. data/test/unit/tag_test.rb +15 -0
  102. data/test/unit/tagging_test.rb +6 -0
  103. data/test/unit/user_test.rb +56 -0
  104. metadata +197 -0
@@ -0,0 +1,752 @@
1
+ module ThoughtBot # :nodoc:
2
+ module Shoulda # :nodoc:
3
+ module ActiveRecord # :nodoc:
4
+ module MacroHelpers # :nodoc:
5
+ # Helper method that determines the default error message used by Active
6
+ # Record. Works for both existing Rails 2.1 and Rails 2.2 with the newly
7
+ # introduced I18n module used for localization.
8
+ #
9
+ # default_error_message(:blank)
10
+ # default_error_message(:too_short, :count => 5)
11
+ # default_error_message(:too_long, :count => 60)
12
+ def default_error_message(key, values = {})
13
+ if Object.const_defined?(:I18n) # Rails >= 2.2
14
+ I18n.translate("activerecord.errors.messages.#{key}", values)
15
+ else # Rails <= 2.1.x
16
+ ::ActiveRecord::Errors.default_error_messages[key] % values[:count]
17
+ end
18
+ end
19
+ end
20
+
21
+ # = Macro test helpers for your active record models
22
+ #
23
+ # These helpers will test most of the validations and associations for your ActiveRecord models.
24
+ #
25
+ # class UserTest < Test::Unit::TestCase
26
+ # should_require_attributes :name, :phone_number
27
+ # should_not_allow_values_for :phone_number, "abcd", "1234"
28
+ # should_allow_values_for :phone_number, "(123) 456-7890"
29
+ #
30
+ # should_protect_attributes :password
31
+ #
32
+ # should_have_one :profile
33
+ # should_have_many :dogs
34
+ # should_have_many :messes, :through => :dogs
35
+ # should_belong_to :lover
36
+ # end
37
+ #
38
+ # For all of these helpers, the last parameter may be a hash of options.
39
+ #
40
+ module Macros
41
+ include MacroHelpers
42
+
43
+ # <b>DEPRECATED:</b> Use <tt>fixtures :all</tt> instead
44
+ #
45
+ # Loads all fixture files (<tt>test/fixtures/*.yml</tt>)
46
+ def load_all_fixtures
47
+ warn "[DEPRECATION] load_all_fixtures is deprecated. Use `fixtures :all` instead."
48
+ fixtures :all
49
+ end
50
+
51
+ # Validates that all fixtures of the current class are valid.
52
+ #
53
+ # Example:
54
+ # should_have_valid_fixtures
55
+ def should_have_valid_fixtures
56
+ klass = model_class
57
+ should "have valid fixtures" do
58
+ klass.all.each do |object|
59
+ valid = object.valid?
60
+ assert valid, "Fixture #{object.inspect} is invalid: #{object.errors.full_messages}"
61
+ end
62
+ end
63
+ end
64
+
65
+ # Ensures that the model cannot be saved if one of the attributes listed is not present.
66
+ #
67
+ # If an instance variable has been created in the setup named after the
68
+ # model being tested, then this method will use that. Otherwise, it will
69
+ # create a new instance to test against.
70
+ #
71
+ # Options:
72
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
73
+ # Regexp or string. Default = <tt>I18n.translate('activerecord.errors.messages.blank')</tt>
74
+ #
75
+ # Example:
76
+ # should_require_attributes :name, :phone_number
77
+ #
78
+ def should_require_attributes(*attributes)
79
+ message = get_options!(attributes, :message)
80
+ message ||= default_error_message(:blank)
81
+ klass = model_class
82
+
83
+ attributes.each do |attribute|
84
+ should "require #{attribute} to be set" do
85
+ assert_bad_value(klass, attribute, nil, message)
86
+ end
87
+ end
88
+ end
89
+
90
+ # Ensures that the model cannot be saved if one of the attributes listed is not unique.
91
+ # Requires an existing record
92
+ #
93
+ # Options:
94
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
95
+ # Regexp or string. Default = <tt>I18n.translate('activerecord.errors.messages.taken')</tt>
96
+ # * <tt>:scoped_to</tt> - field(s) to scope the uniqueness to.
97
+ #
98
+ # Examples:
99
+ # should_require_unique_attributes :keyword, :username
100
+ # should_require_unique_attributes :name, :message => "O NOES! SOMEONE STOELED YER NAME!"
101
+ # should_require_unique_attributes :email, :scoped_to => :name
102
+ # should_require_unique_attributes :address, :scoped_to => [:first_name, :last_name]
103
+ #
104
+ def should_require_unique_attributes(*attributes)
105
+ message, scope = get_options!(attributes, :message, :scoped_to)
106
+ scope = [*scope].compact
107
+ message ||= default_error_message(:taken)
108
+
109
+ klass = model_class
110
+ attributes.each do |attribute|
111
+ attribute = attribute.to_sym
112
+ should "require unique value for #{attribute}#{" scoped to #{scope.join(', ')}" unless scope.blank?}" do
113
+ assert existing = klass.find(:first), "Can't find first #{klass}"
114
+ object = klass.new
115
+ existing_value = existing.send(attribute)
116
+
117
+ if !scope.blank?
118
+ scope.each do |s|
119
+ assert_respond_to object, :"#{s}=", "#{klass.name} doesn't seem to have a #{s} attribute."
120
+ object.send("#{s}=", existing.send(s))
121
+ end
122
+ end
123
+ assert_bad_value(object, attribute, existing_value, message)
124
+
125
+ # Now test that the object is valid when changing the scoped attribute
126
+ # TODO: There is a chance that we could change the scoped field
127
+ # to a value that's already taken. An alternative implementation
128
+ # could actually find all values for scope and create a unique
129
+ # one.
130
+ if !scope.blank?
131
+ scope.each do |s|
132
+ # Assume the scope is a foreign key if the field is nil
133
+ object.send("#{s}=", existing.send(s).nil? ? 1 : existing.send(s).next)
134
+ assert_good_value(object, attribute, existing_value, message)
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
140
+
141
+ # Ensures that the attribute cannot be set on mass update.
142
+ #
143
+ # should_protect_attributes :password, :admin_flag
144
+ #
145
+ def should_protect_attributes(*attributes)
146
+ get_options!(attributes)
147
+ klass = model_class
148
+
149
+ attributes.each do |attribute|
150
+ attribute = attribute.to_sym
151
+ should "protect #{attribute} from mass updates" do
152
+ protected = klass.protected_attributes || []
153
+ accessible = klass.accessible_attributes || []
154
+
155
+ assert protected.include?(attribute.to_s) ||
156
+ (!accessible.empty? && !accessible.include?(attribute.to_s)),
157
+ (accessible.empty? ?
158
+ "#{klass} is protecting #{protected.to_a.to_sentence}, but not #{attribute}." :
159
+ "#{klass} has made #{attribute} accessible")
160
+ end
161
+ end
162
+ end
163
+
164
+ # Ensures that the attribute can be set on mass update.
165
+ #
166
+ # should_allow_attributes :name, :email
167
+ #
168
+ def should_allow_attributes(*attributes)
169
+ get_options!(attributes)
170
+ klass = model_class
171
+
172
+ attributes.each do |attribute|
173
+ attribute = attribute.to_sym
174
+ should "allow #{attribute} from mass updates" do
175
+ protected = klass.protected_attributes || []
176
+ accessible = klass.accessible_attributes || []
177
+
178
+ assert accessible.include?(attribute.to_s) ||
179
+ (!protected.empty? && !protected.include?(attribute.to_s)),
180
+ (protected.empty? ?
181
+ "#{klass} is allowing #{accessible.to_a.to_sentence}, but not #{attribute}." :
182
+ "#{klass} has made #{attribute} protected")
183
+ end
184
+ end
185
+ end
186
+
187
+ # Ensures that the attribute cannot be changed once the record has been created.
188
+ #
189
+ # should_have_readonly_attributes :password, :admin_flag
190
+ #
191
+ def should_have_readonly_attributes(*attributes)
192
+ get_options!(attributes)
193
+ klass = model_class
194
+
195
+ attributes.each do |attribute|
196
+ attribute = attribute.to_sym
197
+ should "make #{attribute} read-only" do
198
+ readonly = klass.readonly_attributes || []
199
+
200
+ assert readonly.include?(attribute.to_s),
201
+ (readonly.empty? ?
202
+ "#{klass} attribute #{attribute} is not read-only" :
203
+ "#{klass} is making #{readonly.to_a.to_sentence} read-only, but not #{attribute}.")
204
+ end
205
+ end
206
+ end
207
+
208
+ # Ensures that the attribute cannot be set to the given values
209
+ #
210
+ # If an instance variable has been created in the setup named after the
211
+ # model being tested, then this method will use that. Otherwise, it will
212
+ # create a new instance to test against.
213
+ #
214
+ # Options:
215
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
216
+ # Regexp or string. Default = <tt>I18n.translate('activerecord.errors.messages.invalid')</tt>
217
+ #
218
+ # Example:
219
+ # should_not_allow_values_for :isbn, "bad 1", "bad 2"
220
+ #
221
+ def should_not_allow_values_for(attribute, *bad_values)
222
+ message = get_options!(bad_values, :message)
223
+ message ||= default_error_message(:invalid)
224
+ klass = model_class
225
+ bad_values.each do |v|
226
+ should "not allow #{attribute} to be set to #{v.inspect}" do
227
+ assert_bad_value(klass, attribute, v, message)
228
+ end
229
+ end
230
+ end
231
+
232
+ # Ensures that the attribute can be set to the given values.
233
+ #
234
+ # If an instance variable has been created in the setup named after the
235
+ # model being tested, then this method will use that. Otherwise, it will
236
+ # create a new instance to test against.
237
+ #
238
+ # Example:
239
+ # should_allow_values_for :isbn, "isbn 1 2345 6789 0", "ISBN 1-2345-6789-0"
240
+ #
241
+ def should_allow_values_for(attribute, *good_values)
242
+ get_options!(good_values)
243
+ klass = model_class
244
+ good_values.each do |v|
245
+ should "allow #{attribute} to be set to #{v.inspect}" do
246
+ assert_good_value(klass, attribute, v)
247
+ end
248
+ end
249
+ end
250
+
251
+ # Ensures that the length of the attribute is in the given range
252
+ #
253
+ # If an instance variable has been created in the setup named after the
254
+ # model being tested, then this method will use that. Otherwise, it will
255
+ # create a new instance to test against.
256
+ #
257
+ # Options:
258
+ # * <tt>:short_message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
259
+ # Regexp or string. Default = <tt>I18n.translate('activerecord.errors.messages.too_short') % range.first</tt>
260
+ # * <tt>:long_message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
261
+ # Regexp or string. Default = <tt>I18n.translate('activerecord.errors.messages.too_long') % range.last</tt>
262
+ #
263
+ # Example:
264
+ # should_ensure_length_in_range :password, (6..20)
265
+ #
266
+ def should_ensure_length_in_range(attribute, range, opts = {})
267
+ short_message, long_message = get_options!([opts], :short_message, :long_message)
268
+ short_message ||= default_error_message(:too_short, :count => range.first)
269
+ long_message ||= default_error_message(:too_long, :count => range.last)
270
+
271
+ klass = model_class
272
+ min_length = range.first
273
+ max_length = range.last
274
+ same_length = (min_length == max_length)
275
+
276
+ if min_length > 0
277
+ should "not allow #{attribute} to be less than #{min_length} chars long" do
278
+ min_value = "x" * (min_length - 1)
279
+ assert_bad_value(klass, attribute, min_value, short_message)
280
+ end
281
+ end
282
+
283
+ if min_length > 0
284
+ should "allow #{attribute} to be exactly #{min_length} chars long" do
285
+ min_value = "x" * min_length
286
+ assert_good_value(klass, attribute, min_value, short_message)
287
+ end
288
+ end
289
+
290
+ should "not allow #{attribute} to be more than #{max_length} chars long" do
291
+ max_value = "x" * (max_length + 1)
292
+ assert_bad_value(klass, attribute, max_value, long_message)
293
+ end
294
+
295
+ unless same_length
296
+ should "allow #{attribute} to be exactly #{max_length} chars long" do
297
+ max_value = "x" * max_length
298
+ assert_good_value(klass, attribute, max_value, long_message)
299
+ end
300
+ end
301
+ end
302
+
303
+ # Ensures that the length of the attribute is at least a certain length
304
+ #
305
+ # If an instance variable has been created in the setup named after the
306
+ # model being tested, then this method will use that. Otherwise, it will
307
+ # create a new instance to test against.
308
+ #
309
+ # Options:
310
+ # * <tt>:short_message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
311
+ # Regexp or string. Default = <tt>I18n.translate('activerecord.errors.messages.too_short') % min_length</tt>
312
+ #
313
+ # Example:
314
+ # should_ensure_length_at_least :name, 3
315
+ #
316
+ def should_ensure_length_at_least(attribute, min_length, opts = {})
317
+ short_message = get_options!([opts], :short_message)
318
+ short_message ||= default_error_message(:too_short, :count => min_length)
319
+
320
+ klass = model_class
321
+
322
+ if min_length > 0
323
+ min_value = "x" * (min_length - 1)
324
+ should "not allow #{attribute} to be less than #{min_length} chars long" do
325
+ assert_bad_value(klass, attribute, min_value, short_message)
326
+ end
327
+ end
328
+ should "allow #{attribute} to be at least #{min_length} chars long" do
329
+ valid_value = "x" * (min_length)
330
+ assert_good_value(klass, attribute, valid_value, short_message)
331
+ end
332
+ end
333
+
334
+ # Ensures that the length of the attribute is exactly a certain length
335
+ #
336
+ # If an instance variable has been created in the setup named after the
337
+ # model being tested, then this method will use that. Otherwise, it will
338
+ # create a new instance to test against.
339
+ #
340
+ # Options:
341
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
342
+ # Regexp or string. Default = <tt>I18n.translate('activerecord.errors.messages.wrong_length') % length</tt>
343
+ #
344
+ # Example:
345
+ # should_ensure_length_is :ssn, 9
346
+ #
347
+ def should_ensure_length_is(attribute, length, opts = {})
348
+ message = get_options!([opts], :message)
349
+ message ||= default_error_message(:wrong_length, :count => length)
350
+
351
+ klass = model_class
352
+
353
+ should "not allow #{attribute} to be less than #{length} chars long" do
354
+ min_value = "x" * (length - 1)
355
+ assert_bad_value(klass, attribute, min_value, message)
356
+ end
357
+
358
+ should "not allow #{attribute} to be greater than #{length} chars long" do
359
+ max_value = "x" * (length + 1)
360
+ assert_bad_value(klass, attribute, max_value, message)
361
+ end
362
+
363
+ should "allow #{attribute} to be #{length} chars long" do
364
+ valid_value = "x" * (length)
365
+ assert_good_value(klass, attribute, valid_value, message)
366
+ end
367
+ end
368
+
369
+ # Ensure that the attribute is in the range specified
370
+ #
371
+ # If an instance variable has been created in the setup named after the
372
+ # model being tested, then this method will use that. Otherwise, it will
373
+ # create a new instance to test against.
374
+ #
375
+ # Options:
376
+ # * <tt>:low_message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
377
+ # Regexp or string. Default = <tt>I18n.translate('activerecord.errors.messages.inclusion')</tt>
378
+ # * <tt>:high_message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
379
+ # Regexp or string. Default = <tt>I18n.translate('activerecord.errors.messages.inclusion')</tt>
380
+ #
381
+ # Example:
382
+ # should_ensure_value_in_range :age, (0..100)
383
+ #
384
+ def should_ensure_value_in_range(attribute, range, opts = {})
385
+ low_message, high_message = get_options!([opts], :low_message, :high_message)
386
+ low_message ||= default_error_message(:inclusion)
387
+ high_message ||= default_error_message(:inclusion)
388
+
389
+ klass = model_class
390
+ min = range.first
391
+ max = range.last
392
+
393
+ should "not allow #{attribute} to be less than #{min}" do
394
+ v = min - 1
395
+ assert_bad_value(klass, attribute, v, low_message)
396
+ end
397
+
398
+ should "allow #{attribute} to be #{min}" do
399
+ v = min
400
+ assert_good_value(klass, attribute, v, low_message)
401
+ end
402
+
403
+ should "not allow #{attribute} to be more than #{max}" do
404
+ v = max + 1
405
+ assert_bad_value(klass, attribute, v, high_message)
406
+ end
407
+
408
+ should "allow #{attribute} to be #{max}" do
409
+ v = max
410
+ assert_good_value(klass, attribute, v, high_message)
411
+ end
412
+ end
413
+
414
+ # Ensure that the attribute is numeric
415
+ #
416
+ # If an instance variable has been created in the setup named after the
417
+ # model being tested, then this method will use that. Otherwise, it will
418
+ # create a new instance to test against.
419
+ #
420
+ # Options:
421
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
422
+ # Regexp or string. Default = <tt>I18n.translate('activerecord.errors.messages.not_a_number')</tt>
423
+ #
424
+ # Example:
425
+ # should_only_allow_numeric_values_for :age
426
+ #
427
+ def should_only_allow_numeric_values_for(*attributes)
428
+ message = get_options!(attributes, :message)
429
+ message ||= default_error_message(:not_a_number)
430
+ klass = model_class
431
+ attributes.each do |attribute|
432
+ attribute = attribute.to_sym
433
+ should "only allow numeric values for #{attribute}" do
434
+ assert_bad_value(klass, attribute, "abcd", message)
435
+ end
436
+ end
437
+ end
438
+
439
+ # Ensures that the has_many relationship exists. Will also test that the
440
+ # associated table has the required columns. Works with polymorphic
441
+ # associations.
442
+ #
443
+ # Options:
444
+ # * <tt>:through</tt> - association name for <tt>has_many :through</tt>
445
+ # * <tt>:dependent</tt> - tests that the association makes use of the dependent option.
446
+ #
447
+ # Example:
448
+ # should_have_many :friends
449
+ # should_have_many :enemies, :through => :friends
450
+ # should_have_many :enemies, :dependent => :destroy
451
+ #
452
+ def should_have_many(*associations)
453
+ through, dependent = get_options!(associations, :through, :dependent)
454
+ klass = model_class
455
+ associations.each do |association|
456
+ name = "have many #{association}"
457
+ name += " through #{through}" if through
458
+ name += " dependent => #{dependent}" if dependent
459
+ should name do
460
+ reflection = klass.reflect_on_association(association)
461
+ assert reflection, "#{klass.name} does not have any relationship to #{association}"
462
+ assert_equal :has_many, reflection.macro
463
+
464
+ if through
465
+ through_reflection = klass.reflect_on_association(through)
466
+ assert through_reflection, "#{klass.name} does not have any relationship to #{through}"
467
+ assert_equal(through, reflection.options[:through])
468
+ end
469
+
470
+ if dependent
471
+ assert_equal dependent.to_s,
472
+ reflection.options[:dependent].to_s,
473
+ "#{association} should have #{dependent} dependency"
474
+ end
475
+
476
+ # Check for the existence of the foreign key on the other table
477
+ unless reflection.options[:through]
478
+ if reflection.options[:foreign_key]
479
+ fk = reflection.options[:foreign_key]
480
+ elsif reflection.options[:as]
481
+ fk = reflection.options[:as].to_s.foreign_key
482
+ else
483
+ fk = reflection.primary_key_name
484
+ end
485
+
486
+ associated_klass_name = (reflection.options[:class_name] || association.to_s.classify)
487
+ associated_klass = associated_klass_name.constantize
488
+
489
+ assert associated_klass.column_names.include?(fk.to_s),
490
+ "#{associated_klass.name} does not have a #{fk} foreign key."
491
+ end
492
+ end
493
+ end
494
+ end
495
+
496
+ # Ensure that the has_one relationship exists. Will also test that the
497
+ # associated table has the required columns. Works with polymorphic
498
+ # associations.
499
+ #
500
+ # Options:
501
+ # * <tt>:dependent</tt> - tests that the association makes use of the dependent option.
502
+ #
503
+ # Example:
504
+ # should_have_one :god # unless hindu
505
+ #
506
+ def should_have_one(*associations)
507
+ dependent = get_options!(associations, :dependent)
508
+ klass = model_class
509
+ associations.each do |association|
510
+ name = "have one #{association}"
511
+ name += " dependent => #{dependent}" if dependent
512
+ should name do
513
+ reflection = klass.reflect_on_association(association)
514
+ assert reflection, "#{klass.name} does not have any relationship to #{association}"
515
+ assert_equal :has_one, reflection.macro
516
+
517
+ associated_klass = (reflection.options[:class_name] || association.to_s.camelize).constantize
518
+
519
+ if reflection.options[:foreign_key]
520
+ fk = reflection.options[:foreign_key]
521
+ elsif reflection.options[:as]
522
+ fk = reflection.options[:as].to_s.foreign_key
523
+ fk_type = fk.gsub(/_id$/, '_type')
524
+ assert associated_klass.column_names.include?(fk_type),
525
+ "#{associated_klass.name} does not have a #{fk_type} column."
526
+ else
527
+ fk = klass.name.foreign_key
528
+ end
529
+ assert associated_klass.column_names.include?(fk.to_s),
530
+ "#{associated_klass.name} does not have a #{fk} foreign key."
531
+
532
+ if dependent
533
+ assert_equal dependent.to_s,
534
+ reflection.options[:dependent].to_s,
535
+ "#{association} should have #{dependent} dependency"
536
+ end
537
+ end
538
+ end
539
+ end
540
+
541
+ # Ensures that the has_and_belongs_to_many relationship exists, and that the join
542
+ # table is in place.
543
+ #
544
+ # should_have_and_belong_to_many :posts, :cars
545
+ #
546
+ def should_have_and_belong_to_many(*associations)
547
+ get_options!(associations)
548
+ klass = model_class
549
+
550
+ associations.each do |association|
551
+ should "should have and belong to many #{association}" do
552
+ reflection = klass.reflect_on_association(association)
553
+ assert reflection, "#{klass.name} does not have any relationship to #{association}"
554
+ assert_equal :has_and_belongs_to_many, reflection.macro
555
+ table = reflection.options[:join_table]
556
+ assert ::ActiveRecord::Base.connection.tables.include?(table.to_s), "table #{table} doesn't exist"
557
+ end
558
+ end
559
+ end
560
+
561
+ # Ensure that the belongs_to relationship exists.
562
+ #
563
+ # should_belong_to :parent
564
+ #
565
+ def should_belong_to(*associations)
566
+ get_options!(associations)
567
+ klass = model_class
568
+ associations.each do |association|
569
+ should "belong_to #{association}" do
570
+ reflection = klass.reflect_on_association(association)
571
+ assert reflection, "#{klass.name} does not have any relationship to #{association}"
572
+ assert_equal :belongs_to, reflection.macro
573
+
574
+ unless reflection.options[:polymorphic]
575
+ associated_klass = (reflection.options[:class_name] || association.to_s.camelize).constantize
576
+ fk = reflection.options[:foreign_key] || reflection.primary_key_name
577
+ assert klass.column_names.include?(fk.to_s), "#{klass.name} does not have a #{fk} foreign key."
578
+ end
579
+ end
580
+ end
581
+ end
582
+
583
+ # Ensure that the given class methods are defined on the model.
584
+ #
585
+ # should_have_class_methods :find, :destroy
586
+ #
587
+ def should_have_class_methods(*methods)
588
+ get_options!(methods)
589
+ klass = model_class
590
+ methods.each do |method|
591
+ should "respond to class method ##{method}" do
592
+ assert_respond_to klass, method, "#{klass.name} does not have class method #{method}"
593
+ end
594
+ end
595
+ end
596
+
597
+ # Ensure that the given instance methods are defined on the model.
598
+ #
599
+ # should_have_instance_methods :email, :name, :name=
600
+ #
601
+ def should_have_instance_methods(*methods)
602
+ get_options!(methods)
603
+ klass = model_class
604
+ methods.each do |method|
605
+ should "respond to instance method ##{method}" do
606
+ assert_respond_to klass.new, method, "#{klass.name} does not have instance method #{method}"
607
+ end
608
+ end
609
+ end
610
+
611
+ # Ensure that the given columns are defined on the models backing SQL table.
612
+ #
613
+ # should_have_db_columns :id, :email, :name, :created_at
614
+ #
615
+ def should_have_db_columns(*columns)
616
+ column_type = get_options!(columns, :type)
617
+ klass = model_class
618
+ columns.each do |name|
619
+ test_name = "have column #{name}"
620
+ test_name += " of type #{column_type}" if column_type
621
+ should test_name do
622
+ column = klass.columns.detect {|c| c.name == name.to_s }
623
+ assert column, "#{klass.name} does not have column #{name}"
624
+ end
625
+ end
626
+ end
627
+
628
+ # Ensure that the given column is defined on the models backing SQL table. The options are the same as
629
+ # the instance variables defined on the column definition: :precision, :limit, :default, :null,
630
+ # :primary, :type, :scale, and :sql_type.
631
+ #
632
+ # should_have_db_column :email, :type => "string", :default => nil, :precision => nil, :limit => 255,
633
+ # :null => true, :primary => false, :scale => nil, :sql_type => 'varchar(255)'
634
+ #
635
+ def should_have_db_column(name, opts = {})
636
+ klass = model_class
637
+ test_name = "have column named :#{name}"
638
+ test_name += " with options " + opts.inspect unless opts.empty?
639
+ should test_name do
640
+ column = klass.columns.detect {|c| c.name == name.to_s }
641
+ assert column, "#{klass.name} does not have column #{name}"
642
+ opts.each do |k, v|
643
+ assert_equal column.instance_variable_get("@#{k}").to_s, v.to_s, ":#{name} column on table for #{klass} does not match option :#{k}"
644
+ end
645
+ end
646
+ end
647
+
648
+ # Ensures that there are DB indices on the given columns or tuples of columns.
649
+ # Also aliased to should_have_index for readability
650
+ #
651
+ # should_have_indices :email, :name, [:commentable_type, :commentable_id]
652
+ # should_have_index :age
653
+ #
654
+ def should_have_indices(*columns)
655
+ table = model_class.table_name
656
+ indices = ::ActiveRecord::Base.connection.indexes(table).map(&:columns)
657
+
658
+ columns.each do |column|
659
+ should "have index on #{table} for #{column.inspect}" do
660
+ columns = [column].flatten.map(&:to_s)
661
+ assert_contains(indices, columns)
662
+ end
663
+ end
664
+ end
665
+
666
+ alias_method :should_have_index, :should_have_indices
667
+
668
+ # Ensures that the model cannot be saved if one of the attributes listed is not accepted.
669
+ #
670
+ # If an instance variable has been created in the setup named after the
671
+ # model being tested, then this method will use that. Otherwise, it will
672
+ # create a new instance to test against.
673
+ #
674
+ # Options:
675
+ # * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
676
+ # Regexp or string. Default = <tt>I18n.translate('activerecord.errors.messages.accepted')</tt>
677
+ #
678
+ # Example:
679
+ # should_require_acceptance_of :eula
680
+ #
681
+ def should_require_acceptance_of(*attributes)
682
+ message = get_options!(attributes, :message)
683
+ message ||= default_error_message(:accepted)
684
+ klass = model_class
685
+
686
+ attributes.each do |attribute|
687
+ should "require #{attribute} to be accepted" do
688
+ assert_bad_value(klass, attribute, false, message)
689
+ end
690
+ end
691
+ end
692
+
693
+ # Ensures that the model has a method named scope_name that returns a NamedScope object with the
694
+ # proxy options set to the options you supply. scope_name can be either a symbol, or a method
695
+ # call which will be evaled against the model. The eval'd method call has access to all the same
696
+ # instance variables that a should statement would.
697
+ #
698
+ # Options: Any of the options that the named scope would pass on to find.
699
+ #
700
+ # Example:
701
+ #
702
+ # should_have_named_scope :visible, :conditions => {:visible => true}
703
+ #
704
+ # Passes for
705
+ #
706
+ # named_scope :visible, :conditions => {:visible => true}
707
+ #
708
+ # Or for
709
+ #
710
+ # def self.visible
711
+ # scoped(:conditions => {:visible => true})
712
+ # end
713
+ #
714
+ # You can test lambdas or methods that return ActiveRecord#scoped calls:
715
+ #
716
+ # should_have_named_scope 'recent(5)', :limit => 5
717
+ # should_have_named_scope 'recent(1)', :limit => 1
718
+ #
719
+ # Passes for
720
+ # named_scope :recent, lambda {|c| {:limit => c}}
721
+ #
722
+ # Or for
723
+ #
724
+ # def self.recent(c)
725
+ # scoped(:limit => c)
726
+ # end
727
+ #
728
+ def should_have_named_scope(scope_call, *args)
729
+ klass = model_class
730
+ scope_opts = args.extract_options!
731
+ scope_call = scope_call.to_s
732
+
733
+ context scope_call do
734
+ setup do
735
+ @scope = eval("#{klass}.#{scope_call}")
736
+ end
737
+
738
+ should "return a scope object" do
739
+ assert_equal ::ActiveRecord::NamedScope::Scope, @scope.class
740
+ end
741
+
742
+ unless scope_opts.empty?
743
+ should "scope itself to #{scope_opts.inspect}" do
744
+ assert_equal scope_opts, @scope.proxy_options
745
+ end
746
+ end
747
+ end
748
+ end
749
+ end
750
+ end
751
+ end
752
+ end