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