module Shoulda # :nodoc:
module ActiveRecord # :nodoc:
# = 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_validate_presence_of :name, :phone_number
# should_not_allow_values_for :phone_number, "abcd", "1234"
# should_allow_values_for :phone_number, "(123) 456-7890"
#
# should_not_allow_mass_assignment_of :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.
#
module Macros
include Helpers
include Matchers
# Ensures that the model cannot be saved if one of the attributes listed is not present.
#
# If an instance variable has been created in the setup named after the
# model being tested, then this method will use that. Otherwise, it will
# create a new instance to test against.
#
# Options:
# * :message - value the test expects to find in errors.on(:attribute).
# Regexp or string. Default = I18n.translate('activerecord.errors.messages.blank')
#
# Example:
# should_validate_presence_of :name, :phone_number
#
def should_validate_presence_of(*attributes)
message = get_options!(attributes, :message)
klass = model_class
attributes.each do |attribute|
matcher = validate_presence_of(attribute).with_message(message)
should matcher.description do
assert_accepts(matcher, get_instance_of(klass))
end
end
end
# Deprecated. See should_validate_presence_of
def should_require_attributes(*attributes)
warn "[DEPRECATION] should_require_attributes is deprecated. " <<
"Use should_validate_presence_of instead."
should_validate_presence_of(*attributes)
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 = I18n.translate('activerecord.errors.messages.taken')
# * :scoped_to - field(s) to scope the uniqueness to.
# * :case_sensitive - whether or not uniqueness is defined by an
# exact match. Ignored by non-text attributes. Default = true
#
# Examples:
# should_validate_uniqueness_of :keyword, :username
# should_validate_uniqueness_of :name, :message => "O NOES! SOMEONE STOELED YER NAME!"
# should_validate_uniqueness_of :email, :scoped_to => :name
# should_validate_uniqueness_of :address, :scoped_to => [:first_name, :last_name]
# should_validate_uniqueness_of :email, :case_sensitive => false
#
def should_validate_uniqueness_of(*attributes)
message, scope, case_sensitive = get_options!(attributes, :message, :scoped_to, :case_sensitive)
scope = [*scope].compact
case_sensitive = true if case_sensitive.nil?
klass = model_class
attributes.each do |attribute|
matcher = validate_uniqueness_of(attribute).
with_message(message).scoped_to(scope)
matcher = matcher.case_insensitive unless case_sensitive
should matcher.description do
assert_accepts(matcher, get_instance_of(klass))
end
end
end
# Deprecated. See should_validate_uniqueness_of
def should_require_unique_attributes(*attributes)
warn "[DEPRECATION] should_require_unique_attributes is deprecated. " <<
"Use should_validate_uniqueness_of instead."
should_validate_uniqueness_of(*attributes)
end
# Ensures that the attribute can be set on mass update.
#
# should_allow_mass_assignment_of :first_name, :last_name
#
def should_allow_mass_assignment_of(*attributes)
get_options!(attributes)
klass = model_class
attributes.each do |attribute|
matcher = allow_mass_assignment_of(attribute)
should matcher.description do
assert_accepts matcher, klass.new
end
end
end
# Ensures that the attribute cannot be set on mass update.
#
# should_not_allow_mass_assignment_of :password, :admin_flag
#
def should_not_allow_mass_assignment_of(*attributes)
get_options!(attributes)
klass = model_class
attributes.each do |attribute|
matcher = allow_mass_assignment_of(attribute)
should "not #{matcher.description}" do
assert_rejects matcher, klass.new
end
end
end
# Deprecated. See should_not_allow_mass_assignment_of
def should_protect_attributes(*attributes)
warn "[DEPRECATION] should_protect_attributes is deprecated. " <<
"Use should_not_allow_mass_assignment_of instead."
should_not_allow_mass_assignment_of(*attributes)
end
# Ensures that the attribute cannot be changed once the record has been created.
#
# should_have_readonly_attributes :password, :admin_flag
#
def should_have_readonly_attributes(*attributes)
get_options!(attributes)
klass = model_class
attributes.each do |attribute|
matcher = have_readonly_attribute(attribute)
should matcher.description do
assert_accepts matcher, klass.new
end
end
end
# Ensures that the attribute cannot be set to the given values
#
# If an instance variable has been created in the setup named after the
# model being tested, then this method will use that. Otherwise, it will
# create a new instance to test against.
#
# Options:
# * :message - value the test expects to find in errors.on(:attribute).
# Regexp or string. Default = I18n.translate('activerecord.errors.messages.invalid')
#
# Example:
# should_not_allow_values_for :isbn, "bad 1", "bad 2"
#
def should_not_allow_values_for(attribute, *bad_values)
message = get_options!(bad_values, :message)
klass = model_class
bad_values.each do |value|
matcher = allow_value(value).for(attribute).with_message(message)
should "not #{matcher.description}" do
assert_rejects matcher, get_instance_of(klass)
end
end
end
# Ensures that the attribute can be set to the given values.
#
# If an instance variable has been created in the setup named after the
# model being tested, then this method will use that. Otherwise, it will
# create a new instance to test against.
#
# Example:
# should_allow_values_for :isbn, "isbn 1 2345 6789 0", "ISBN 1-2345-6789-0"
#
def should_allow_values_for(attribute, *good_values)
get_options!(good_values)
klass = model_class
klass = model_class
good_values.each do |value|
matcher = allow_value(value).for(attribute)
should matcher.description do
assert_accepts matcher, get_instance_of(klass)
end
end
end
# Ensures that the length of the attribute is in the given range
#
# If an instance variable has been created in the setup named after the
# model being tested, then this method will use that. Otherwise, it will
# create a new instance to test against.
#
# Options:
# * :short_message - value the test expects to find in errors.on(:attribute).
# Regexp or string. Default = I18n.translate('activerecord.errors.messages.too_short') % range.first
# * :long_message - value the test expects to find in errors.on(:attribute).
# Regexp or string. Default = I18n.translate('activerecord.errors.messages.too_long') % range.last
#
# Example:
# should_ensure_length_in_range :password, (6..20)
#
def should_ensure_length_in_range(attribute, range, opts = {})
short_message, long_message = get_options!([opts],
:short_message,
:long_message)
klass = model_class
matcher = ensure_length_of(attribute).
is_at_least(range.first).
with_short_message(short_message).
is_at_most(range.last).
with_long_message(long_message)
should matcher.description do
assert_accepts matcher, get_instance_of(klass)
end
end
# Ensures that the length of the attribute is at least a certain length
#
# If an instance variable has been created in the setup named after the
# model being tested, then this method will use that. Otherwise, it will
# create a new instance to test against.
#
# Options:
# * :short_message - value the test expects to find in errors.on(:attribute).
# Regexp or string. Default = I18n.translate('activerecord.errors.messages.too_short') % min_length
#
# Example:
# should_ensure_length_at_least :name, 3
#
def should_ensure_length_at_least(attribute, min_length, opts = {})
short_message = get_options!([opts], :short_message)
klass = model_class
matcher = ensure_length_of(attribute).
is_at_least(min_length).
with_short_message(short_message)
should matcher.description do
assert_accepts matcher, get_instance_of(klass)
end
end
# Ensures that the length of the attribute is exactly a certain length
#
# If an instance variable has been created in the setup named after the
# model being tested, then this method will use that. Otherwise, it will
# create a new instance to test against.
#
# Options:
# * :message - value the test expects to find in errors.on(:attribute).
# Regexp or string. Default = I18n.translate('activerecord.errors.messages.wrong_length') % length
#
# Example:
# should_ensure_length_is :ssn, 9
#
def should_ensure_length_is(attribute, length, opts = {})
message = get_options!([opts], :message)
klass = model_class
matcher = ensure_length_of(attribute).
is_equal_to(length).
with_message(message)
should matcher.description do
assert_accepts matcher, get_instance_of(klass)
end
end
# Ensure that the attribute is in the range specified
#
# If an instance variable has been created in the setup named after the
# model being tested, then this method will use that. Otherwise, it will
# create a new instance to test against.
#
# Options:
# * :low_message - value the test expects to find in errors.on(:attribute).
# Regexp or string. Default = I18n.translate('activerecord.errors.messages.inclusion')
# * :high_message - value the test expects to find in errors.on(:attribute).
# Regexp or string. Default = I18n.translate('activerecord.errors.messages.inclusion')
#
# Example:
# should_ensure_value_in_range :age, (0..100)
#
def should_ensure_value_in_range(attribute, range, opts = {})
message = get_options!([opts], :message)
message ||= default_error_message(:inclusion)
klass = model_class
matcher = ensure_inclusion_of(attribute).
in_range(range).
with_message(message)
should matcher.description do
assert_accepts matcher, get_instance_of(klass)
end
end
# Ensure that the attribute is numeric
#
# If an instance variable has been created in the setup named after the
# model being tested, then this method will use that. Otherwise, it will
# create a new instance to test against.
#
# Options:
# * :message - value the test expects to find in errors.on(:attribute).
# Regexp or string. Default = I18n.translate('activerecord.errors.messages.not_a_number')
#
# Example:
# should_validate_numericality_of :age
#
def should_validate_numericality_of(*attributes)
message = get_options!(attributes, :message)
klass = model_class
attributes.each do |attribute|
matcher = validate_numericality_of(attribute).
with_message(message)
should matcher.description do
assert_accepts matcher, get_instance_of(klass)
end
end
end
# Deprecated. See should_validate_uniqueness_of
def should_only_allow_numeric_values_for(*attributes)
warn "[DEPRECATION] should_only_allow_numeric_values_for is " <<
"deprecated. Use should_validate_numericality_of instead."
should_validate_numericality_of(*attributes)
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
#
def should_have_many(*associations)
through, dependent = get_options!(associations, :through, :dependent)
klass = model_class
associations.each do |association|
matcher = have_many(association).through(through).dependent(dependent)
should matcher.description do
assert_accepts(matcher, klass.new)
end
end
end
# Ensure that the has_one relationship exists. Will also test that the
# associated table has the required columns. Works with polymorphic
# associations.
#
# Options:
# * :dependent - tests that the association makes use of the dependent option.
#
# Example:
# should_have_one :god # unless hindu
#
def should_have_one(*associations)
dependent = get_options!(associations, :dependent)
klass = model_class
associations.each do |association|
matcher = have_one(association).dependent(dependent)
should matcher.description do
assert_accepts(matcher, klass.new)
end
end
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
#
def should_have_and_belong_to_many(*associations)
get_options!(associations)
klass = model_class
associations.each do |association|
matcher = have_and_belong_to_many(association)
should matcher.description do
assert_accepts(matcher, klass.new)
end
end
end
# Ensure that the belongs_to relationship exists.
#
# should_belong_to :parent
#
def should_belong_to(*associations)
dependent = get_options!(associations, :dependent)
klass = model_class
associations.each do |association|
matcher = belong_to(association).dependent(dependent)
should matcher.description do
assert_accepts(matcher, klass.new)
end
end
end
# Ensure that the given class methods are defined on the model.
#
# should_have_class_methods :find, :destroy
#
def should_have_class_methods(*methods)
get_options!(methods)
klass = model_class
methods.each do |method|
should "respond to class method ##{method}" do
assert_respond_to klass, method, "#{klass.name} does not have class method #{method}"
end
end
end
# Ensure that the given instance methods are defined on the model.
#
# should_have_instance_methods :email, :name, :name=
#
def should_have_instance_methods(*methods)
get_options!(methods)
klass = model_class
methods.each do |method|
should "respond to instance method ##{method}" do
assert_respond_to klass.new, method, "#{klass.name} does not have instance method #{method}"
end
end
end
# Ensure that the given columns are defined on the models backing SQL table.
# Also aliased to should_have_index for readability.
# Takes the same options available in migrations:
# :type, :precision, :limit, :default, :null, and :scale
#
# Examples:
#
# should_have_db_columns :id, :email, :name, :created_at
#
# should_have_db_column :email, :type => "string", :limit => 255
# should_have_db_column :salary, :decimal, :precision => 15, :scale => 2
# should_have_db_column :admin, :default => false, :null => false
#
def should_have_db_columns(*columns)
column_type, precision, limit, default, null, scale, sql_type =
get_options!(columns, :type, :precision, :limit,
:default, :null, :scale, :sql_type)
klass = model_class
columns.each do |name|
matcher = have_db_column(name).
of_type(column_type).
with_options(:precision => precision, :limit => limit,
:default => default, :null => null,
:scale => scale, :sql_type => sql_type)
should matcher.description do
assert_accepts(matcher, klass.new)
end
end
end
alias_method :should_have_db_column, :should_have_db_columns
# Ensures that there are DB indices on the given columns or tuples of columns.
# Also aliased to should_have_index for readability
#
# Options:
# * :unique - whether or not the index has a unique
# constraint. Use true to explicitly test for a unique
# constraint. Use false to explicitly test for a non-unique
# constraint. Use nil if you don't care whether the index is
# unique or not. Default = nil
#
# Examples:
#
# should_have_indices :email, :name, [:commentable_type, :commentable_id]
# should_have_index :age
# should_have_index :ssn, :unique => true
#
def should_have_indices(*columns)
unique = get_options!(columns, :unique)
klass = model_class
columns.each do |column|
matcher = have_index(column).unique(unique)
should matcher.description do
assert_accepts(matcher, klass.new)
end
end
end
alias_method :should_have_index, :should_have_indices
# Ensures that the model cannot be saved if one of the attributes listed is not accepted.
#
# If an instance variable has been created in the setup named after the
# model being tested, then this method will use that. Otherwise, it will
# create a new instance to test against.
#
# Options:
# * :message - value the test expects to find in errors.on(:attribute).
# Regexp or string. Default = I18n.translate('activerecord.errors.messages.accepted')
#
# Example:
# should_validate_acceptance_of :eula
#
def should_validate_acceptance_of(*attributes)
message = get_options!(attributes, :message)
klass = model_class
attributes.each do |attribute|
matcher = validate_acceptance_of(attribute).with_message(message)
should matcher.description do
assert_accepts matcher, get_instance_of(klass)
end
end
end
# Deprecated. See should_validate_uniqueness_of
def should_require_acceptance_of(*attributes)
warn "[DEPRECATION] should_require_acceptance_of is deprecated. " <<
"Use should_validate_acceptance_of instead."
should_validate_acceptance_of(*attributes)
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
#
def should_have_named_scope(scope_call, find_options = nil)
klass = model_class
matcher = have_named_scope(scope_call).finding(find_options)
should matcher.description do
assert_accepts matcher.in_context(self), klass.new
end
end
end
end
end