- refactored into modules

- added documentation


git-svn-id: https://svn.thoughtbot.com/plugins/tb_test_helpers/trunk@78 7bbfaf0e-4d1d-0410-9690-a8bb5f8ef2aa
This commit is contained in:
tsaleh 2007-04-06 14:14:07 +00:00
parent 3e8ee72fbd
commit 021d72775e
4 changed files with 517 additions and 415 deletions

View File

@ -1,250 +1,333 @@
module Test # :nodoc:
module Unit # :nodoc:
class TestCase
class << self
# Ensures that the model cannot be saved if one of the attributes listed is not present.
# Requires an existing record
def should_require_attributes(*attributes)
opts = opts_from(attributes)
opts[:message] ||= /blank/
klass = model_class
attributes.each do |attribute|
should "require #{attribute} to be set" do
object = klass.new
assert !object.valid?, "Instance is still valid"
assert object.errors.on(attribute), "No errors found"
assert_contains(object.errors.on(attribute), opts[:message])
end
end
end
# Ensures that the model cannot be saved if one of the attributes listed is not unique.
# Requires an existing record
def should_require_unique_attributes(*attributes)
opts = opts_from(attributes)
opts[:message] ||= /taken/
scope = opts[:scoped_to]
klass = model_class
attributes.each do |attribute|
attribute = attribute.to_sym
should "require unique value for #{attribute}#{" scoped to #{scope}" if scope}" do
assert existing = klass.find(:first), "Can't find first #{klass}"
object = klass.new
object.send(:"#{attribute}=", existing.send(attribute))
if scope
assert_respond_to object, :"#{scope}="
object.send(:"#{scope}=", existing.send(scope))
end
assert !object.valid?, "Instance is still valid"
assert object.errors.on(attribute), "No errors found"
assert_contains(object.errors.on(attribute), opts[:message])
if scope
# Now test that the object is valid when changing the scoped attribute
object.send(:"#{scope}=", existing.send(scope).nil? ? 1 : existing.send(scope).next)
object.errors.clear
object.valid?
assert_does_not_contain(object.errors.on(attribute), opts[:message],
"after :#{scope} set to #{object.send(scope.to_sym)}")
end
end
end
end
# Ensures that the attribute cannot be set on update
# Requires an existing record
def should_protect_attributes(*attributes)
opts = opts_from(attributes)
klass = model_class
attributes.each do |attribute|
attribute = attribute.to_sym
should "not allow #{attribute} to be changed by update" do
assert object = klass.find(:first), "Can't find first #{klass}"
value = object[attribute]
assert object.update_attributes({ attribute => 1 }),
"Cannot update #{klass} with { :#{attribute} => 1 }, #{object.errors.full_messages.to_sentence}"
assert object.valid?, "#{klass} isn't valid after changing #{attribute}"
assert_equal value, object[attribute], "Was able to change #{klass}##{attribute}"
end
end
end
# Ensures that the attribute cannot be set to the given values
# Requires an existing record
def should_not_allow_values_for(attribute, *bad_values)
opts = opts_from(bad_values)
opts[:message] ||= /invalid/
klass = model_class
bad_values.each do |v|
should "not allow #{attribute} to be set to \"#{v}\"" do
assert object = klass.find(:first), "Can't find first #{klass}"
object.send("#{attribute}=", v)
assert !object.save, "Saved #{klass} with #{attribute} set to \"#{v}\""
assert object.errors.on(attribute), "There are no errors set on #{attribute} after being set to \"#{v}\""
assert_contains(object.errors.on(attribute), opts[:message], "when set to \"#{v}\"")
end
end
end
# Ensures that the attribute can be set to the given values.
# Requires an existing record
def should_allow_values_for(attribute, *good_values)
opts = opts_from(good_values)
opts[:message] ||= /invalid/
klass = model_class
good_values.each do |v|
should "allow #{attribute} to be set to \"#{v}\"" do
assert object = klass.find(:first), "Can't find first #{klass}"
object.send("#{attribute}=", v)
object.save
# assert object.errors.on(attribute), "There are no errors set on #{attribute} after being set to \"#{v}\""
assert_contains(object.errors.on(attribute), opts[:messgae], "when set to \"#{v}\"")
end
end
end
# Ensures that the length of the attribute is in the given range
# Requires an existing record
def should_ensure_length_in_range(attribute, range, opts = {})
opts[:short_message] ||= /short/
opts[:long_message] ||= /long/
klass = model_class
min_length = range.first
max_length = range.last
min_value = "x" * (min_length - 1)
max_value = "x" * (max_length + 1)
should "not allow #{attribute} to be less than #{min_length} chars long" do
assert object = klass.find(:first), "Can't find first #{klass}"
object.send("#{attribute}=", min_value)
assert !object.save, "Saved #{klass} with #{attribute} set to \"#{min_value}\""
assert object.errors.on(attribute), "There are no errors set on #{attribute} after being set to \"#{min_value}\""
assert_contains(object.errors.on(attribute), opts[:short_message], "when set to \"#{min_value}\"")
end
should "not allow #{attribute} to be more than #{max_length} chars long" do
assert object = klass.find(:first), "Can't find first #{klass}"
object.send("#{attribute}=", max_value)
assert !object.save, "Saved #{klass} with #{attribute} set to \"#{max_value}\""
assert object.errors.on(attribute), "There are no errors set on #{attribute} after being set to \"#{max_value}\""
assert_contains(object.errors.on(attribute), opts[:long_message], "when set to \"#{max_value}\"")
end
end
# Ensure that the attribute is in the range specified
# Requires an existing record
def should_ensure_value_in_range(attribute, range, opts = {})
opts[:low_message] ||= /included/
opts[:high_message] ||= /included/
klass = model_class
min = range.first
max = range.last
should "not allow #{attribute} to be less than #{min}" do
v = min - 1
assert object = klass.find(:first), "Can't find first #{klass}"
object.send("#{attribute}=", v)
assert !object.save, "Saved #{klass} with #{attribute} set to \"#{v}\""
assert object.errors.on(attribute), "There are no errors set on #{attribute} after being set to \"#{v}\""
assert_contains(object.errors.on(attribute), opts[:low_message], "when set to \"#{v}\"")
end
should "not allow #{attribute} to be more than #{max}" do
v = max + 1
assert object = klass.find(:first), "Can't find first #{klass}"
object.send("#{attribute}=", v)
assert !object.save, "Saved #{klass} with #{attribute} set to \"#{v}\""
assert object.errors.on(attribute), "There are no errors set on #{attribute} after being set to \"#{v}\""
assert_contains(object.errors.on(attribute), opts[:high_message], "when set to \"#{v}\"")
end
end
module ThoughtBot # :nodoc:
module TestHelpers # :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
# 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.
#
module ActiveRecord
# Ensures that the model cannot be saved if one of the attributes listed is not present.
# Requires an existing record.
#
# Options:
# * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
# Regexp or string. Default = <tt>/blank/</tt>
#
# Example:
# should_require_attributes :name, :phone_number
def should_require_attributes(*attributes)
opts = opts_from(attributes)
opts[:message] ||= /blank/
klass = model_class
# Ensure that the attribute is numeric
# Requires an existing record
def should_only_allow_numeric_values_for(*attributes)
opts = opts_from(attributes)
opts[:message] ||= /number/
klass = model_class
attributes.each do |attribute|
attribute = attribute.to_sym
should "only allow numeric values for #{attribute}" do
assert object = klass.find(:first), "Can't find first #{klass}"
object.send(:"#{attribute}=", "abcd")
assert !object.valid?, "Instance is still valid"
assert_contains(object.errors.on(attribute), opts[:message])
end
attributes.each do |attribute|
should "require #{attribute} to be set" do
object = klass.new
assert !object.valid?, "Instance is still valid"
assert object.errors.on(attribute), "No errors found"
assert_contains(object.errors.on(attribute), opts[:message])
end
end
# Ensures that the has_many relationship exists.
# The last parameter may be a hash of options. Currently, the only supported option
# is :through
def should_have_many(*associations)
opts = opts_from(associations)
klass = model_class
associations.each do |association|
should "have many #{association}#{" through #{opts[:through]}" if opts[:through]}" do
reflection = klass.reflect_on_association(association)
assert reflection
assert_equal :has_many, reflection.macro
assert_equal(opts[:through], reflection.options[:through]) if opts[:through]
end
end
end
# Ensures that the has_and_belongs_to_many relationship exists.
def should_have_and_belong_to_many(*associations)
opts = opts_from(associations)
klass = model_class
associations.each do |association|
should "should have and belong to many #{association}" do
assert klass.reflect_on_association(association)
assert_equal :has_and_belongs_to_many, klass.reflect_on_association(association).macro
end
end
end
# Ensure that the has_one relationship exists.
def should_have_one(*associations)
opts = opts_from(associations)
klass = model_class
associations.each do |association|
should "have one #{association}" do
assert klass.reflect_on_association(association)
assert_equal :has_one, klass.reflect_on_association(association).macro
end
end
end
# Ensure that the belongs_to relationship exists.
def should_belong_to(*associations)
opts = opts_from(associations)
klass = model_class
associations.each do |association|
should "belong_to #{association}" do
assert klass.reflect_on_association(association)
assert_equal :belongs_to, klass.reflect_on_association(association).macro
end
end
end
private
def opts_from(collection)
collection.last.is_a?(Hash) ? collection.pop : {}
end
def model_class
self.name.gsub(/Test$/, '').constantize
end
end
# Ensures that the model cannot be saved if one of the attributes listed is not unique.
# Requires an existing record
#
# Options:
# * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
# Regexp or string. Default = <tt>/taken/</tt>
#
# Example:
# should_require_unique_attributes :keyword, :username
def should_require_unique_attributes(*attributes)
opts = opts_from(attributes)
opts[:message] ||= /taken/
scope = opts[:scoped_to]
klass = model_class
attributes.each do |attribute|
attribute = attribute.to_sym
should "require unique value for #{attribute}#{" scoped to #{scope}" if scope}" do
assert existing = klass.find(:first), "Can't find first #{klass}"
object = klass.new
object.send(:"#{attribute}=", existing.send(attribute))
if scope
assert_respond_to object, :"#{scope}="
object.send(:"#{scope}=", existing.send(scope))
end
assert !object.valid?, "Instance is still valid"
assert object.errors.on(attribute), "No errors found"
assert_contains(object.errors.on(attribute), opts[:message])
if scope
# Now test that the object is valid when changing the scoped attribute
object.send(:"#{scope}=", existing.send(scope).nil? ? 1 : existing.send(scope).next)
object.errors.clear
object.valid?
assert_does_not_contain(object.errors.on(attribute), opts[:message],
"after :#{scope} set to #{object.send(scope.to_sym)}")
end
end
end
end
# Ensures that the attribute cannot be set on update
# Requires an existing record
#
# should_protect_attributes :password, :admin_flag
def should_protect_attributes(*attributes)
opts = opts_from(attributes)
klass = model_class
attributes.each do |attribute|
attribute = attribute.to_sym
should "not allow #{attribute} to be changed by update" do
assert object = klass.find(:first), "Can't find first #{klass}"
value = object[attribute]
assert object.update_attributes({ attribute => 1 }),
"Cannot update #{klass} with { :#{attribute} => 1 }, #{object.errors.full_messages.to_sentence}"
assert object.valid?, "#{klass} isn't valid after changing #{attribute}"
assert_equal value, object[attribute], "Was able to change #{klass}##{attribute}"
end
end
end
# Ensures that the attribute cannot be set to the given values
# Requires an existing record
#
# Options:
# * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
# Regexp or string. Default = <tt>/invalid/</tt>
#
# Example:
# should_not_allow_values_for :isbn, "bad 1", "bad 2"
def should_not_allow_values_for(attribute, *bad_values)
opts = opts_from(bad_values)
opts[:message] ||= /invalid/
klass = model_class
bad_values.each do |v|
should "not allow #{attribute} to be set to \"#{v}\"" do
assert object = klass.find(:first), "Can't find first #{klass}"
object.send("#{attribute}=", v)
assert !object.save, "Saved #{klass} with #{attribute} set to \"#{v}\""
assert object.errors.on(attribute), "There are no errors set on #{attribute} after being set to \"#{v}\""
assert_contains(object.errors.on(attribute), opts[:message], "when set to \"#{v}\"")
end
end
end
# Ensures that the attribute can be set to the given values.
# Requires an existing record
#
# Options:
# * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
# Regexp or string. Default = <tt>/invalid/</tt>
#
# Example:
# should_allow_values_for :isbn, "isbn 1 2345 6789 0", "ISBN 1-2345-6789-0"
def should_allow_values_for(attribute, *good_values)
opts = opts_from(good_values)
opts[:message] ||= /invalid/
klass = model_class
good_values.each do |v|
should "allow #{attribute} to be set to \"#{v}\"" do
assert object = klass.find(:first), "Can't find first #{klass}"
object.send("#{attribute}=", v)
object.save
# assert object.errors.on(attribute), "There are no errors set on #{attribute} after being set to \"#{v}\""
assert_contains(object.errors.on(attribute), opts[:messgae], "when set to \"#{v}\"")
end
end
end
# Ensures that the length of the attribute is in the given range
# Requires an existing record
#
# Options:
# * <tt>:short_message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
# Regexp or string. Default = <tt>/short/</tt>
# * <tt>:long_message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
# Regexp or string. Default = <tt>/long/</tt>
#
# Example:
# should_ensure_length_in_range :password, (6..20)
def should_ensure_length_in_range(attribute, range, opts = {})
opts[:short_message] ||= /short/
opts[:long_message] ||= /long/
klass = model_class
min_length = range.first
max_length = range.last
min_value = "x" * (min_length - 1)
max_value = "x" * (max_length + 1)
should "not allow #{attribute} to be less than #{min_length} chars long" do
assert object = klass.find(:first), "Can't find first #{klass}"
object.send("#{attribute}=", min_value)
assert !object.save, "Saved #{klass} with #{attribute} set to \"#{min_value}\""
assert object.errors.on(attribute), "There are no errors set on #{attribute} after being set to \"#{min_value}\""
assert_contains(object.errors.on(attribute), opts[:short_message], "when set to \"#{min_value}\"")
end
should "not allow #{attribute} to be more than #{max_length} chars long" do
assert object = klass.find(:first), "Can't find first #{klass}"
object.send("#{attribute}=", max_value)
assert !object.save, "Saved #{klass} with #{attribute} set to \"#{max_value}\""
assert object.errors.on(attribute), "There are no errors set on #{attribute} after being set to \"#{max_value}\""
assert_contains(object.errors.on(attribute), opts[:long_message], "when set to \"#{max_value}\"")
end
end
# Ensure that the attribute is in the range specified
# Requires an existing record
#
# Options:
# * <tt>:low_message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
# Regexp or string. Default = <tt>/included/</tt>
# * <tt>:high_message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
# Regexp or string. Default = <tt>/included/</tt>
#
# Example:
# should_ensure_value_in_range :age, (0..100)
def should_ensure_value_in_range(attribute, range, opts = {})
opts[:low_message] ||= /included/
opts[:high_message] ||= /included/
klass = model_class
min = range.first
max = range.last
should "not allow #{attribute} to be less than #{min}" do
v = min - 1
assert object = klass.find(:first), "Can't find first #{klass}"
object.send("#{attribute}=", v)
assert !object.save, "Saved #{klass} with #{attribute} set to \"#{v}\""
assert object.errors.on(attribute), "There are no errors set on #{attribute} after being set to \"#{v}\""
assert_contains(object.errors.on(attribute), opts[:low_message], "when set to \"#{v}\"")
end
should "not allow #{attribute} to be more than #{max}" do
v = max + 1
assert object = klass.find(:first), "Can't find first #{klass}"
object.send("#{attribute}=", v)
assert !object.save, "Saved #{klass} with #{attribute} set to \"#{v}\""
assert object.errors.on(attribute), "There are no errors set on #{attribute} after being set to \"#{v}\""
assert_contains(object.errors.on(attribute), opts[:high_message], "when set to \"#{v}\"")
end
end
# Ensure that the attribute is numeric
# Requires an existing record
#
# Options:
# * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
# Regexp or string. Default = <tt>/number/</tt>
#
# Example:
# should_only_allow_numeric_values_for :age
def should_only_allow_numeric_values_for(*attributes)
opts = opts_from(attributes)
opts[:message] ||= /number/
klass = model_class
attributes.each do |attribute|
attribute = attribute.to_sym
should "only allow numeric values for #{attribute}" do
assert object = klass.find(:first), "Can't find first #{klass}"
object.send(:"#{attribute}=", "abcd")
assert !object.valid?, "Instance is still valid"
assert_contains(object.errors.on(attribute), opts[:message])
end
end
end
# Ensures that the has_many relationship exists.
#
# Options:
# * <tt>:through</tt> - association name for <tt>has_many :through</tt>
#
# Example:
# should_have_many :friends
# should_have_many :enemies, :through => :friends
def should_have_many(*associations)
opts = opts_from(associations)
klass = model_class
associations.each do |association|
should "have many #{association}#{" through #{opts[:through]}" if opts[:through]}" do
reflection = klass.reflect_on_association(association)
assert reflection
assert_equal :has_many, reflection.macro
assert_equal(opts[:through], reflection.options[:through]) if opts[:through]
end
end
end
# Ensures that the has_and_belongs_to_many relationship exists.
#
# should_have_and_belong_to_many :posts, :cars
def should_have_and_belong_to_many(*associations)
opts = opts_from(associations)
klass = model_class
associations.each do |association|
should "should have and belong to many #{association}" do
assert klass.reflect_on_association(association)
assert_equal :has_and_belongs_to_many, klass.reflect_on_association(association).macro
end
end
end
# Ensure that the has_one relationship exists.
#
# should_have_one :god # unless hindu
def should_have_one(*associations)
opts = opts_from(associations)
klass = model_class
associations.each do |association|
should "have one #{association}" do
assert klass.reflect_on_association(association)
assert_equal :has_one, klass.reflect_on_association(association).macro
end
end
end
# Ensure that the belongs_to relationship exists.
#
# should_belong_to :parent
def should_belong_to(*associations)
opts = opts_from(associations)
klass = model_class
associations.each do |association|
should "belong_to #{association}" do
assert klass.reflect_on_association(association)
assert_equal :belongs_to, klass.reflect_on_association(association).macro
end
end
end
private
def opts_from(collection)
collection.last.is_a?(Hash) ? collection.pop : {}
end
def model_class
self.name.gsub(/Test$/, '').constantize
end
end
end
end

View File

@ -1,120 +1,122 @@
module TBTestHelpers # :nodoc:
# = context and should blocks
#
# A context block can exist next to normal <tt>def test_blah</tt> statements,
# meaning you do not have to fully commit to the context/should syntax in a test file. We have been
# using this syntax at ThoughtBot, though, and find it very readable.
#
# A context block can contain setup, should, should_eventually, and teardown blocks.
#
# class UserTest << Test::Unit
# context "a User instance" do
# setup do
# @user = User.find(:first)
# end
#
# should "return its full name"
# assert_equal 'John Doe', @user.full_name
# end
# end
# end
#
# This code will produce the method <tt>"test a User instance should return its full name"</tt> (yes, with spaces in the name).
#
# Contexts may be nested. Nested contexts run their setup blocks from out to in before each test.
# They then run their teardown blocks from in to out after each test.
#
# class UserTest << Test::Unit
# context "a User instance" do
# setup do
# @user = User.find(:first)
# end
#
# should "return its full name"
# assert_equal 'John Doe', @user.full_name
# end
#
# context "with a profile" do
# setup do
# @user.profile = Profile.find(:first)
# end
#
# should "return true when sent :has_profile?"
# assert @user.has_profile?
# end
# end
# end
# end
#
# This code will produce the following methods
# * <tt>"test a User instance should return its full name"</tt>
# * <tt>"test a User instance with a profile should return true when sent :has_profile?"</tt> (which will have both setup blocks run before it.)
#
module ThoughtBot # :nodoc:
module TestHelpers # :nodoc:
# = context and should blocks
#
# A context block can exist next to normal <tt>def test_blah</tt> statements,
# meaning you do not have to fully commit to the context/should syntax in a test file. We have been
# using this syntax at ThoughtBot, though, and find it very readable.
#
# A context block can contain setup, should, should_eventually, and teardown blocks.
#
# class UserTest << Test::Unit
# context "a User instance" do
# setup do
# @user = User.find(:first)
# end
#
# should "return its full name"
# assert_equal 'John Doe', @user.full_name
# end
# end
# end
#
# This code will produce the method <tt>"test a User instance should return its full name"</tt> (yes, with spaces in the name).
#
# Contexts may be nested. Nested contexts run their setup blocks from out to in before each test.
# They then run their teardown blocks from in to out after each test.
#
# class UserTest << Test::Unit
# context "a User instance" do
# setup do
# @user = User.find(:first)
# end
#
# should "return its full name"
# assert_equal 'John Doe', @user.full_name
# end
#
# context "with a profile" do
# setup do
# @user.profile = Profile.find(:first)
# end
#
# should "return true when sent :has_profile?"
# assert @user.has_profile?
# end
# end
# end
# end
#
# This code will produce the following methods
# * <tt>"test a User instance should return its full name"</tt>
# * <tt>"test a User instance with a profile should return true when sent :has_profile?"</tt> (which will have both setup blocks run before it.)
#
module Context
def Context.included(other) # :nodoc:
@@context_names = []
@@setup_blocks = []
@@teardown_blocks = []
end
module Context
def Context.included(other) # :nodoc:
@@context_names = []
@@setup_blocks = []
@@teardown_blocks = []
end
# Creates a context block with the given name.
def context(name, &context_block)
saved_setups = @@setup_blocks.dup
saved_teardowns = @@teardown_blocks.dup
saved_contexts = @@context_names.dup
# Creates a context block with the given name.
def context(name, &context_block)
saved_setups = @@setup_blocks.dup
saved_teardowns = @@teardown_blocks.dup
saved_contexts = @@context_names.dup
@@context_names << name
context_block.bind(self).call
@@context_names << name
context_block.bind(self).call
@@context_names = saved_contexts
@@setup_blocks = saved_setups
@@teardown_blocks = saved_teardowns
end
@@context_names = saved_contexts
@@setup_blocks = saved_setups
@@teardown_blocks = saved_teardowns
end
# Run before every should block in the current context
def setup(&setup_block)
@@setup_blocks << setup_block
end
# Run before every should block in the current context
def setup(&setup_block)
@@setup_blocks << setup_block
end
# Run after every should block in the current context
def teardown(&teardown_block)
@@teardown_blocks << teardown_block
end
# Run after every should block in the current context
def teardown(&teardown_block)
@@teardown_blocks << teardown_block
end
# Defines a test. Can be called either inside our outside of a context.
# Optionally specify <tt>:unimplimented => true</tt> (see should_eventually)
def should(name, opts = {}, &should_block)
test_name = ["test", @@context_names, "should", "#{name}"].flatten.join(' ').to_sym
# Defines a test. Can be called either inside our outside of a context.
# Optionally specify <tt>:unimplimented => true</tt> (see should_eventually)
def should(name, opts = {}, &should_block)
test_name = ["test", @@context_names, "should", "#{name}"].flatten.join(' ').to_sym
name_defined = eval("self.instance_methods.include?('#{test_name.to_s.gsub(/['"]/, '\$1')}')", should_block.binding)
raise ArgumentError, "'#{test_name}' is already defined" and return if name_defined
name_defined = eval("self.instance_methods.include?('#{test_name.to_s.gsub(/['"]/, '\$1')}')", should_block.binding)
raise ArgumentError, "'#{test_name}' is already defined" and return if name_defined
setup_blocks = @@setup_blocks.dup
teardown_blocks = @@teardown_blocks.dup
setup_blocks = @@setup_blocks.dup
teardown_blocks = @@teardown_blocks.dup
if opts[:unimplemented]
define_method test_name do |*args|
# XXX find a better way of doing this.
assert true
STDOUT.putc "X" # Tests for this model are missing.
end
else
define_method test_name do |*args|
begin
setup_blocks.each {|b| b.bind(self).call }
should_block.bind(self).call(*args)
ensure
teardown_blocks.reverse.each {|b| b.bind(self).call }
if opts[:unimplemented]
define_method test_name do |*args|
# XXX find a better way of doing this.
assert true
STDOUT.putc "X" # Tests for this model are missing.
end
else
define_method test_name do |*args|
begin
setup_blocks.each {|b| b.bind(self).call }
should_block.bind(self).call(*args)
ensure
teardown_blocks.reverse.each {|b| b.bind(self).call }
end
end
end
end
end
# Defines a specification that is not yet implemented.
# Will be displayed as an 'X' when running tests, and failures will not be shown.
def should_eventually(name, &block)
should("eventually #{name}", {:unimplemented => true}, &block)
# Defines a specification that is not yet implemented.
# Will be displayed as an 'X' when running tests, and failures will not be shown.
def should_eventually(name, &block)
should("eventually #{name}", {:unimplemented => true}, &block)
end
end
end
end

79
lib/general.rb Normal file
View File

@ -0,0 +1,79 @@
module ThoughtBot # :nodoc:
module TestHelpers # :nodoc:
module General # :nodoc:
module ClassMethods
# Loads all fixture files (<tt>test/fixtures/*.yml</tt>)
def load_all_fixtures
all_fixtures = Dir.glob(File.join(RAILS_ROOT, "test", "fixtures", "*.yml")).collect do |f|
File.basename(f, '.yml').to_sym
end
fixtures *all_fixtures
end
end
module InstanceMethods
# Prints a message to stdout, tagged with the name of the calling method.
def report!(msg = "")
puts("#{caller.first}: #{msg}")
end
# Ensures that the number of items in the collection changes
#
# assert_difference(User, :count, 1) { User.create }
# assert_difference(User.packages, :size, 3, true) { User.add_three_packages }
#
# Setting reload to true will call <tt>object.reload</tt> after the block (for ActiveRecord associations)
def assert_difference(object, method, difference, reload = false, msg = nil)
initial_value = object.send(method)
yield
object.send(:reload) if reload
assert_equal initial_value + difference, object.send(method), (msg || "#{object}##{method} after block")
end
# Ensures that object.method does not change. See assert_difference for usage.
def assert_no_difference(object, method, reload = false, msg = nil, &block)
assert_difference(object, method, 0, reload, msg, &block)
end
# Asserts that two arrays contain the same elements, the same number of times. Essentially ==, but unordered.
#
# assert_same_elements([:a, :b, :c], [:c, :a, :b]) => passes
def assert_same_elements(a1, a2, msg = nil)
[:select, :inject, :size].each do |m|
[a1, a2].each {|a| assert_respond_to(a, m, "Are you sure that #{a.inspect} is an array? It doesn't respond to #{m}.") }
end
assert a1h = a1.inject({}) { |h,e| h[e] = a1.select { |i| i == e }.size; h }
assert a2h = a2.inject({}) { |h,e| h[e] = a2.select { |i| i == e }.size; h }
assert_equal(a1h, a2h, msg)
end
# Asserts that the given collection contains item x. If x is a regular expression, ensure that
# at least one element from the collection matches x. +extra_msg+ is appended to the error message if the assertion fails.
#
# assert_contains(['a', '1'], /\d/) => passes
def assert_contains(collection, x, extra_msg = "")
collection = [collection] unless collection.is_a?(Array)
msg = "#{x} not found in #{collection.to_a.inspect} " + extra_msg
case x
when Regexp: assert(collection.detect { |e| e =~ x }, msg)
else assert(collection.include?(x), msg)
end
end
# Asserts that the given collection does not contain item x. If x is a regular expression, ensure that
# none of the elements from the collection match x.
def assert_does_not_contain(collection, x, extra_msg = "")
collection = [collection] unless collection.is_a?(Array)
msg = "#{x} found in #{collection.to_a.inspect} " + extra_msg
case x
when Regexp: assert(!collection.detect { |e| e =~ x }, msg)
else assert(!collection.include?(x), msg)
end
end
end
end
end
end

View File

@ -1,5 +1,6 @@
require 'active_record_helpers'
require 'context'
require 'general'
require 'yaml'
config_file = "tb_test_helpers.conf"
@ -11,77 +12,14 @@ require 'color' if tb_test_options[:color]
module Test # :nodoc:
module Unit # :nodoc:
class TestCase
include ThoughtBot::TestHelpers::General::InstanceMethods
class << self
include TBTestHelpers::Context
# Loads all fixture files (<tt>test/fixtures/*.yml</tt>)
def load_all_fixtures
all_fixtures = Dir.glob(File.join(RAILS_ROOT, "test", "fixtures", "*.yml")).collect do |f|
File.basename(f, '.yml').to_sym
end
fixtures *all_fixtures
end
include ThoughtBot::TestHelpers::Context
include ThoughtBot::TestHelpers::ActiveRecord
include ThoughtBot::TestHelpers::General::ClassMethods
end
# Prints a message to stdout, tagged with the name of the calling method.
def report!(msg = "")
puts("#{caller.first}: #{msg}")
end
# Ensures that the number of items in the collection changes
# assert_difference(User, :count, 1) { User.create }
# assert_difference(User.packages, :size, 3, true) { User.add_three_packages }
#
# Setting reload to true will call <tt>object.reload</tt> after the block (for ActiveRecord associations)
def assert_difference(object, method, difference, reload = false, msg = nil)
initial_value = object.send(method)
yield
object.send(:reload) if reload
assert_equal initial_value + difference, object.send(method), (msg || "#{object}##{method} after block")
end
# Ensures that object.method does not change. See assert_difference for usage.
def assert_no_difference(object, method, reload = false, msg = nil, &block)
assert_difference(object, method, 0, reload, msg, &block)
end
# Asserts that two arrays contain the same elements, the same number of times. Essentially ==, but unordered.
# assert_same_elements([:a, :b, :c], [:c, :a, :b]) => passes
def assert_same_elements(a1, a2, msg = nil)
[:select, :inject, :size].each do |m|
[a1, a2].each {|a| assert_respond_to(a, m, "Are you sure that #{a.inspect} is an array? It doesn't respond to #{m}.") }
end
assert a1h = a1.inject({}) { |h,e| h[e] = a1.select { |i| i == e }.size; h }
assert a2h = a2.inject({}) { |h,e| h[e] = a2.select { |i| i == e }.size; h }
assert_equal(a1h, a2h, msg)
end
# Asserts that the given collection contains item x. If x is a regular expression, ensure that
# at least one element from the collection matches x.
# assert_contains(['a', '1'], /\d/) => passes
def assert_contains(collection, x, extra_msg = "")
collection = [collection] unless collection.is_a?(Array)
msg = "#{x} not found in #{collection.to_a.inspect} " + extra_msg
case x
when Regexp: assert(collection.detect { |e| e =~ x }, msg)
else assert(collection.include?(x), msg)
end
end
# Asserts that the given collection does not contain item x. If x is a regular expression, ensure that
# none of the elements from the collection match x.
def assert_does_not_contain(collection, x, extra_msg = "")
collection = [collection] unless collection.is_a?(Array)
msg = "#{x} found in #{collection.to_a.inspect} " + extra_msg
case x
when Regexp: assert(!collection.detect { |e| e =~ x }, msg)
else assert(!collection.include?(x), msg)
end
end
end
end
end