Module ThoughtBot::Shoulda::ActiveRecord
In: lib/shoulda/active_record_helpers.rb

Macro test helpers for your active record models

These helpers will test most of the validations and associations for your ActiveRecord models.

  class UserTest < Test::Unit::TestCase
    should_require_attributes :name, :phone_number
    should_not_allow_values_for :phone_number, "abcd", "1234"
    should_allow_values_for :phone_number, "(123) 456-7890"

    should_protect_attributes :password

    should_have_one :profile
    should_have_many :dogs
    should_have_many :messes, :through => :dogs
    should_belong_to :lover
  end

For all of these helpers, the last parameter may be a hash of options.

Methods

Included Modules

ThoughtBot::Shoulda::Private

Public Instance methods

Ensures that the attribute can be set to the given values. Requires an existing record

Example:

  should_allow_values_for :isbn, "isbn 1 2345 6789 0", "ISBN 1-2345-6789-0"

[Source]

     # File lib/shoulda/active_record_helpers.rb, line 167
167:       def should_allow_values_for(attribute, *good_values)
168:         get_options!(good_values)
169:         klass = model_class
170:         good_values.each do |v|
171:           should "allow #{attribute} to be set to #{v.inspect}" do
172:             assert_good_value(klass, attribute, v)
173:           end
174:         end
175:       end

Ensure that the belongs_to relationship exists.

  should_belong_to :parent

[Source]

     # File lib/shoulda/active_record_helpers.rb, line 467
467:       def should_belong_to(*associations)
468:         get_options!(associations)
469:         klass = model_class
470:         associations.each do |association|
471:           should "belong_to #{association}" do
472:             reflection = klass.reflect_on_association(association)
473:             assert reflection, "#{klass.name} does not have any relationship to #{association}"
474:             assert_equal :belongs_to, reflection.macro
475: 
476:             unless reflection.options[:polymorphic]
477:               associated_klass = (reflection.options[:class_name] || association.to_s.classify).constantize
478:               fk = reflection.options[:foreign_key] || reflection.primary_key_name
479:               assert klass.column_names.include?(fk.to_s), "#{klass.name} does not have a #{fk} foreign key."
480:             end
481:           end
482:         end
483:       end

Ensures that the length of the attribute is at least a certain length Requires an existing record

Options:

  • :short_message - value the test expects to find in errors.on(:attribute). Regexp or string. Default = /short/

Example:

  should_ensure_length_at_least :name, 3

[Source]

     # File lib/shoulda/active_record_helpers.rb, line 236
236:       def should_ensure_length_at_least(attribute, min_length, opts = {})
237:         short_message = get_options!([opts], :short_message)
238:         short_message ||= /short/
239: 
240:         klass = model_class
241: 
242:         if min_length > 0
243:           min_value = "x" * (min_length - 1)
244:           should "not allow #{attribute} to be less than #{min_length} chars long" do
245:             assert_bad_value(klass, attribute, min_value, short_message)
246:           end
247:         end
248:         should "allow #{attribute} to be at least #{min_length} chars long" do
249:           valid_value = "x" * (min_length)
250:           assert_good_value(klass, attribute, valid_value, short_message)
251:         end
252:       end

Ensures that the length of the attribute is in the given range Requires an existing record

Options:

  • :short_message - value the test expects to find in errors.on(:attribute). Regexp or string. Default = /short/
  • :long_message - value the test expects to find in errors.on(:attribute). Regexp or string. Default = /long/

Example:

  should_ensure_length_in_range :password, (6..20)

[Source]

     # File lib/shoulda/active_record_helpers.rb, line 189
189:       def should_ensure_length_in_range(attribute, range, opts = {})
190:         short_message, long_message = get_options!([opts], :short_message, :long_message)
191:         short_message ||= /short/
192:         long_message  ||= /long/
193: 
194:         klass = model_class
195:         min_length = range.first
196:         max_length = range.last
197:         same_length = (min_length == max_length)
198: 
199:         if min_length > 0
200:           should "not allow #{attribute} to be less than #{min_length} chars long" do
201:             min_value = "x" * (min_length - 1)
202:             assert_bad_value(klass, attribute, min_value, short_message)
203:           end
204:         end
205: 
206:         if min_length > 0
207:           should "allow #{attribute} to be exactly #{min_length} chars long" do
208:             min_value = "x" * min_length
209:             assert_good_value(klass, attribute, min_value, short_message)
210:           end
211:         end
212: 
213:         should "not allow #{attribute} to be more than #{max_length} chars long" do
214:           max_value = "x" * (max_length + 1)
215:           assert_bad_value(klass, attribute, max_value, long_message)
216:         end
217: 
218:         unless same_length
219:           should "allow #{attribute} to be exactly #{max_length} chars long" do
220:             max_value = "x" * max_length
221:             assert_good_value(klass, attribute, max_value, long_message)
222:           end
223:         end
224:       end

Ensures that the length of the attribute is exactly a certain length Requires an existing record

Options:

  • :message - value the test expects to find in errors.on(:attribute). Regexp or string. Default = /short/

Example:

  should_ensure_length_is :ssn, 9

[Source]

     # File lib/shoulda/active_record_helpers.rb, line 264
264:       def should_ensure_length_is(attribute, length, opts = {})
265:         message = get_options!([opts], :message)
266:         message ||= /wrong length/
267: 
268:         klass = model_class
269: 
270:         should "not allow #{attribute} to be less than #{length} chars long" do
271:           min_value = "x" * (length - 1)
272:           assert_bad_value(klass, attribute, min_value, message)
273:         end
274: 
275:         should "not allow #{attribute} to be greater than #{length} chars long" do
276:           max_value = "x" * (length + 1)
277:           assert_bad_value(klass, attribute, max_value, message)
278:         end
279: 
280:         should "allow #{attribute} to be #{length} chars long" do
281:           valid_value = "x" * (length)
282:           assert_good_value(klass, attribute, valid_value, message)
283:         end
284:       end

Ensure that the attribute is in the range specified Requires an existing record

Options:

  • :low_message - value the test expects to find in errors.on(:attribute). Regexp or string. Default = /included/
  • :high_message - value the test expects to find in errors.on(:attribute). Regexp or string. Default = /included/

Example:

  should_ensure_value_in_range :age, (0..100)

[Source]

     # File lib/shoulda/active_record_helpers.rb, line 298
298:       def should_ensure_value_in_range(attribute, range, opts = {})
299:         low_message, high_message = get_options!([opts], :low_message, :high_message)
300:         low_message  ||= /included/
301:         high_message ||= /included/
302: 
303:         klass = model_class
304:         min   = range.first
305:         max   = range.last
306: 
307:         should "not allow #{attribute} to be less than #{min}" do
308:           v = min - 1
309:           assert_bad_value(klass, attribute, v, low_message)
310:         end
311: 
312:         should "allow #{attribute} to be #{min}" do
313:           v = min
314:           assert_good_value(klass, attribute, v, low_message)
315:         end
316: 
317:         should "not allow #{attribute} to be more than #{max}" do
318:           v = max + 1
319:           assert_bad_value(klass, attribute, v, high_message)
320:         end
321: 
322:         should "allow #{attribute} to be #{max}" do
323:           v = max
324:           assert_good_value(klass, attribute, v, high_message)
325:         end
326:       end

Ensures that the has_and_belongs_to_many relationship exists, and that the join table is in place.

  should_have_and_belong_to_many :posts, :cars

[Source]

     # File lib/shoulda/active_record_helpers.rb, line 448
448:       def should_have_and_belong_to_many(*associations)
449:         get_options!(associations)
450:         klass = model_class
451: 
452:         associations.each do |association|
453:           should "should have and belong to many #{association}" do
454:             reflection = klass.reflect_on_association(association)
455:             assert reflection, "#{klass.name} does not have any relationship to #{association}"
456:             assert_equal :has_and_belongs_to_many, reflection.macro
457:             table = reflection.options[:join_table]
458:             assert ::ActiveRecord::Base.connection.tables.include?(table), "table #{table} doesn't exist"
459:           end
460:         end
461:       end

Ensure that the given class methods are defined on the model.

  should_have_class_methods :find, :destroy

[Source]

     # File lib/shoulda/active_record_helpers.rb, line 489
489:       def should_have_class_methods(*methods)
490:         get_options!(methods)
491:         klass = model_class
492:         methods.each do |method|
493:           should "respond to class method ##{method}" do
494:             assert_respond_to klass, method, "#{klass.name} does not have class method #{method}"
495:           end
496:         end
497:       end

Ensure that the given column is defined on the models backing SQL table. The options are the same as the instance variables defined on the column definition: :precision, :limit, :default, :null, :primary, :type, :scale, and :sql_type.

  should_have_db_column :email, :type => "string", :default => nil,   :precision => nil, :limit    => 255,
                                :null => true,     :primary => false, :scale     => nil, :sql_type => 'varchar(255)'

[Source]

     # File lib/shoulda/active_record_helpers.rb, line 537
537:       def should_have_db_column(name, opts = {})
538:         klass = model_class
539:         test_name = "have column named :#{name}"
540:         test_name += " with options " + opts.inspect unless opts.empty?
541:         should test_name do
542:           column = klass.columns.detect {|c| c.name == name.to_s }
543:           assert column, "#{klass.name} does not have column #{name}"
544:           opts.each do |k, v|
545:             assert_equal column.instance_variable_get("@#{k}").to_s, v.to_s, ":#{name} column on table for #{klass} does not match option :#{k}"
546:           end
547:         end
548:       end

Ensure that the given columns are defined on the models backing SQL table.

  should_have_db_columns :id, :email, :name, :created_at

[Source]

     # File lib/shoulda/active_record_helpers.rb, line 517
517:       def should_have_db_columns(*columns)
518:         column_type = get_options!(columns, :type)
519:         klass = model_class
520:         columns.each do |name|
521:           test_name = "have column #{name}"
522:           test_name += " of type #{column_type}" if column_type
523:           should test_name do
524:             column = klass.columns.detect {|c| c.name == name.to_s }
525:             assert column, "#{klass.name} does not have column #{name}"
526:           end
527:         end
528:       end
should_have_index(*columns)

Ensures that there are DB indices on the given columns or tuples of columns. Also aliased to should_have_index for readability

  should_have_indices :email, :name, [:commentable_type, :commentable_id]
  should_have_index :age

[Source]

     # File lib/shoulda/active_record_helpers.rb, line 556
556:       def should_have_indices(*columns)
557:         table = model_class.name.tableize
558:         indices = ::ActiveRecord::Base.connection.indexes(table).map(&:columns)
559: 
560:         columns.each do |column|
561:           should "have index on #{table} for #{column.inspect}" do
562:             columns = [column].flatten.map(&:to_s)
563:             assert_contains(indices, columns)
564:           end
565:         end
566:       end

Ensure that the given instance methods are defined on the model.

  should_have_instance_methods :email, :name, :name=

[Source]

     # File lib/shoulda/active_record_helpers.rb, line 503
503:       def should_have_instance_methods(*methods)
504:         get_options!(methods)
505:         klass = model_class
506:         methods.each do |method|
507:           should "respond to instance method ##{method}" do
508:             assert_respond_to klass.new, method, "#{klass.name} does not have instance method #{method}"
509:           end
510:         end
511:       end

Ensures that the has_many relationship exists. Will also test that the associated table has the required columns. Works with polymorphic associations.

Options:

  • :through - association name for has_many :through
  • :dependent - tests that the association makes use of the dependent option.

Example:

  should_have_many :friends
  should_have_many :enemies, :through => :friends
  should_have_many :enemies, :dependent => :destroy

[Source]

     # File lib/shoulda/active_record_helpers.rb, line 363
363:       def should_have_many(*associations)
364:         through, dependent = get_options!(associations, :through, :dependent)
365:         klass = model_class
366:         associations.each do |association|
367:           name = "have many #{association}"
368:           name += " through #{through}" if through
369:           name += " dependent => #{dependent}" if dependent
370:           should name do
371:             reflection = klass.reflect_on_association(association)
372:             assert reflection, "#{klass.name} does not have any relationship to #{association}"
373:             assert_equal :has_many, reflection.macro
374: 
375:             associated_klass_name = reflection.options[:class_name]
376:             associated_klass_name = reflection.options[:source].to_s.classify if associated_klass_name.blank?
377:             associated_klass_name = association.to_s.classify                 if associated_klass_name.blank?
378:             associated_klass = associated_klass_name.constantize
379: 
380:             if through
381:               through_reflection = klass.reflect_on_association(through)
382:               assert through_reflection, "#{klass.name} does not have any relationship to #{through}"
383:               assert_equal(through, reflection.options[:through])
384:             end
385: 
386:             if dependent
387:               assert_equal dependent.to_s,
388:                            reflection.options[:dependent].to_s,
389:                            "#{associated_klass.name} should have #{dependent} dependency"
390:             end
391: 
392:             # Check for the existence of the foreign key on the other table
393:             unless reflection.options[:through]
394:               if reflection.options[:foreign_key]
395:                 fk = reflection.options[:foreign_key]
396:               elsif reflection.options[:as]
397:                 fk = reflection.options[:as].to_s.foreign_key
398:               else
399:                 fk = reflection.primary_key_name
400:               end
401: 
402:               assert associated_klass.column_names.include?(fk.to_s),
403:                      "#{associated_klass.name} does not have a #{fk} foreign key."
404:             end
405:           end
406:         end
407:       end

Ensures that the model has a method named scope_name that returns a NamedScope object with the proxy options set to the options you supply. scope_name can be either a symbol, or a method call which will be evaled against the model. The eval‘d method call has access to all the same instance variables that a should statement would.

Options: Any of the options that the named scope would pass on to find.

Example:

  should_have_named_scope :visible, :conditions => {:visible => true}

Passes for

  named_scope :visible, :conditions => {:visible => true}

Or for

  def self.visible
    scoped(:conditions => {:visible => true})
  end

You can test lambdas or methods that return ActiveRecord#scoped calls:

  should_have_named_scope 'recent(5)', :limit => 5
  should_have_named_scope 'recent(1)', :limit => 1

Passes for

  named_scope :recent, lambda {|c| {:limit => c}}

Or for

  def self.recent(c)
    scoped(:limit => c)
  end

[Source]

     # File lib/shoulda/active_record_helpers.rb, line 626
626:       def should_have_named_scope(scope_call, *args)
627:         klass = model_class
628:         scope_opts = args.extract_options!
629:         scope_call = scope_call.to_s
630: 
631:         context scope_call do
632:           setup do
633:             @scope = eval("#{klass}.#{scope_call}")
634:           end
635: 
636:           should "return a scope object" do
637:             assert_equal ::ActiveRecord::NamedScope::Scope, @scope.class
638:           end
639: 
640:           unless scope_opts.empty?
641:             should "scope itself to #{scope_opts.inspect}" do
642:               assert_equal scope_opts, @scope.proxy_options
643:             end
644:           end
645:         end
646:       end

Ensure that the has_one relationship exists. Will also test that the associated table has the required columns. Works with polymorphic associations.

Example:

  should_have_one :god # unless hindu

[Source]

     # File lib/shoulda/active_record_helpers.rb, line 416
416:       def should_have_one(*associations)
417:         get_options!(associations)
418:         klass = model_class
419:         associations.each do |association|
420:           should "have one #{association}" do
421:             reflection = klass.reflect_on_association(association)
422:             assert reflection, "#{klass.name} does not have any relationship to #{association}"
423:             assert_equal :has_one, reflection.macro
424: 
425:             associated_klass = (reflection.options[:class_name] || association.to_s.camelize).constantize
426: 
427:             if reflection.options[:foreign_key]
428:               fk = reflection.options[:foreign_key]
429:             elsif reflection.options[:as]
430:               fk = reflection.options[:as].to_s.foreign_key
431:               fk_type = fk.gsub(/_id$/, '_type')
432:               assert associated_klass.column_names.include?(fk_type),
433:                      "#{associated_klass.name} does not have a #{fk_type} column."
434:             else
435:               fk = klass.name.foreign_key
436:             end
437:             assert associated_klass.column_names.include?(fk.to_s),
438:                    "#{associated_klass.name} does not have a #{fk} foreign key."
439:           end
440:         end
441:       end

Ensures that the attribute cannot be changed once the record has been created. Requires an existing record.

  should_have_readonly_attributes :password, :admin_flag

[Source]

     # File lib/shoulda/active_record_helpers.rb, line 123
123:       def should_have_readonly_attributes(*attributes)
124:         get_options!(attributes)
125:         klass = model_class
126: 
127:         attributes.each do |attribute|
128:           attribute = attribute.to_sym
129:           should "make #{attribute} read-only" do
130:             readonly = klass.readonly_attributes || []
131: 
132:             assert readonly.include?(attribute.to_s),
133:                    (readonly.empty? ?
134:                      "#{klass} attribute #{attribute} is not read-only" :
135:                      "#{klass} is making #{readonly.to_a.to_sentence} read-only, but not #{attribute}.")
136:           end
137:         end
138:       end

Ensures that the attribute cannot be set to the given values Requires an existing record

Options:

  • :message - value the test expects to find in errors.on(:attribute). Regexp or string. Default = /invalid/

Example:

  should_not_allow_values_for :isbn, "bad 1", "bad 2"

[Source]

     # File lib/shoulda/active_record_helpers.rb, line 150
150:       def should_not_allow_values_for(attribute, *bad_values)
151:         message = get_options!(bad_values, :message)
152:         message ||= /invalid/
153:         klass = model_class
154:         bad_values.each do |v|
155:           should "not allow #{attribute} to be set to #{v.inspect}" do
156:             assert_bad_value(klass, attribute, v, message)
157:           end
158:         end
159:       end

Ensure that the attribute is numeric Requires an existing record

Options:

  • :message - value the test expects to find in errors.on(:attribute). Regexp or string. Default = /number/

Example:

  should_only_allow_numeric_values_for :age

[Source]

     # File lib/shoulda/active_record_helpers.rb, line 338
338:       def should_only_allow_numeric_values_for(*attributes)
339:         message = get_options!(attributes, :message)
340:         message ||= /number/
341:         klass = model_class
342:         attributes.each do |attribute|
343:           attribute = attribute.to_sym
344:           should "only allow numeric values for #{attribute}" do
345:             assert_bad_value(klass, attribute, "abcd", message)
346:           end
347:         end
348:       end

Ensures that the attribute cannot be set on mass update. Requires an existing record.

  should_protect_attributes :password, :admin_flag

[Source]

     # File lib/shoulda/active_record_helpers.rb, line 100
100:       def should_protect_attributes(*attributes)
101:         get_options!(attributes)
102:         klass = model_class
103: 
104:         attributes.each do |attribute|
105:           attribute = attribute.to_sym
106:           should "protect #{attribute} from mass updates" do
107:             protected = klass.protected_attributes || []
108:             accessible = klass.accessible_attributes || []
109: 
110:             assert protected.include?(attribute.to_s) || !accessible.include?(attribute.to_s),
111:                    (accessible.empty? ?
112:                      "#{klass} is protecting #{protected.to_a.to_sentence}, but not #{attribute}." :
113:                      "#{klass} has made #{attribute} accessible")
114:           end
115:         end
116:       end

Ensures that the model cannot be saved if one of the attributes listed is not accepted.

Options:

  • :message - value the test expects to find in errors.on(:attribute). Regexp or string. Default = /must be accepted/

Example:

  should_require_acceptance_of :eula

[Source]

     # File lib/shoulda/active_record_helpers.rb, line 579
579:       def should_require_acceptance_of(*attributes)
580:         message = get_options!(attributes, :message)
581:         message ||= /must be accepted/
582:         klass = model_class
583: 
584:         attributes.each do |attribute|
585:           should "require #{attribute} to be accepted" do
586:             assert_bad_value(klass, attribute, false, message)
587:           end
588:         end
589:       end

Ensures that the model cannot be saved if one of the attributes listed is not present.

Options:

  • :message - value the test expects to find in errors.on(:attribute). Regexp or string. Default = /blank/

Example:

  should_require_attributes :name, :phone_number

[Source]

    # File lib/shoulda/active_record_helpers.rb, line 32
32:       def should_require_attributes(*attributes)
33:         message = get_options!(attributes, :message)
34:         message ||= /blank/
35:         klass = model_class
36: 
37:         attributes.each do |attribute|
38:           should "require #{attribute} to be set" do
39:             assert_bad_value(klass, attribute, nil, message)
40:           end
41:         end
42:       end

Ensures that the model cannot be saved if one of the attributes listed is not unique. Requires an existing record

Options:

  • :message - value the test expects to find in errors.on(:attribute). Regexp or string. Default = /taken/
  • :scoped_to - field(s) to scope the uniqueness to.

Examples:

  should_require_unique_attributes :keyword, :username
  should_require_unique_attributes :name, :message => "O NOES! SOMEONE STOELED YER NAME!"
  should_require_unique_attributes :email, :scoped_to => :name
  should_require_unique_attributes :address, :scoped_to => [:first_name, :last_name]

[Source]

    # File lib/shoulda/active_record_helpers.rb, line 58
58:       def should_require_unique_attributes(*attributes)
59:         message, scope = get_options!(attributes, :message, :scoped_to)
60:         scope = [*scope].compact
61:         message ||= /taken/
62: 
63:         klass = model_class
64:         attributes.each do |attribute|
65:           attribute = attribute.to_sym
66:           should "require unique value for #{attribute}#{" scoped to #{scope.join(', ')}" unless scope.blank?}" do
67:             assert existing = klass.find(:first), "Can't find first #{klass}"
68:             object = klass.new
69:             existing_value = existing.send(attribute)
70: 
71:             if !scope.blank?
72:               scope.each do |s|
73:                 assert_respond_to object, "#{s}=""#{s}=", "#{klass.name} doesn't seem to have a #{s} attribute."
74:                 object.send("#{s}=", existing.send(s))
75:               end
76:             end
77:             assert_bad_value(object, attribute, existing_value, message)
78: 
79:             # Now test that the object is valid when changing the scoped attribute
80:             # TODO:  There is a chance that we could change the scoped field
81:             # to a value that's already taken.  An alternative implementation
82:             # could actually find all values for scope and create a unique
83:             # one.
84:             if !scope.blank?
85:               scope.each do |s|
86:                 # Assume the scope is a foreign key if the field is nil
87:                 object.send("#{s}=", existing.send(s).nil? ? 1 : existing.send(s).next)
88:                 assert_good_value(object, attribute, existing_value, message)
89:               end
90:             end
91:           end
92:         end
93:       end

[Validate]