mjankowski-shoulda 2.0.4

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