From 730c2d338b09c92c23bd643c9539766afeeaf46c Mon Sep 17 00:00:00 2001 From: August Lilleaas Date: Fri, 13 Jun 2008 14:23:32 +0200 Subject: [PATCH 1/8] Calling should without a block piggybacks to should_eventually. --- lib/shoulda/gem/shoulda.rb | 2 ++ test/other/context_test.rb | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/shoulda/gem/shoulda.rb b/lib/shoulda/gem/shoulda.rb index d1fab1de..d2293716 100644 --- a/lib/shoulda/gem/shoulda.rb +++ b/lib/shoulda/gem/shoulda.rb @@ -34,6 +34,8 @@ module Thoughtbot # Note: The part before should in the test name is gleamed from the name of the Test::Unit class. def should(name, &blk) + should_eventually(name) && return unless block_given? + if Shoulda.current_context Shoulda.current_context.should(name, &blk) else diff --git a/test/other/context_test.rb b/test/other/context_test.rb index 6d931537..2a17e249 100644 --- a/test/other/context_test.rb +++ b/test/other/context_test.rb @@ -67,5 +67,7 @@ class ContextTest < Test::Unit::TestCase # :nodoc: should_eventually "should pass, since it's unimplemented" do flunk "what?" end - + + should_eventually "should not require a block when using should_eventually" + should "should pass without a block, as that causes it to piggyback to should_eventually" end From dbcbacef313145fad452bf195e3b84dd48b29eb6 Mon Sep 17 00:00:00 2001 From: Tammer Saleh Date: Tue, 17 Jun 2008 13:03:25 -0400 Subject: [PATCH 2/8] Added contribution guidelines --- CONTRIBUTION_GUIDELINES.rdoc | 12 ++++++++++++ Rakefile | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 CONTRIBUTION_GUIDELINES.rdoc diff --git a/CONTRIBUTION_GUIDELINES.rdoc b/CONTRIBUTION_GUIDELINES.rdoc new file mode 100644 index 00000000..ac1794f8 --- /dev/null +++ b/CONTRIBUTION_GUIDELINES.rdoc @@ -0,0 +1,12 @@ +We're using GitHub[http://github.com/thoughtbot/shoulda/tree/master] and Lighthouse[http://thoughtbot.lighthouseapp.com/projects/5807], and we've been getting any combination of github pull requests, Lighthouse tickets, patches, emails, etc. We need to normalize this workflow to make sure we don't miss any fixes. + +* Make sure you're accessing the source from the {official repository}[http://github.com/thoughtbot/shoulda/tree/master]. +* We prefer git branches over patches, but we can take either. +* If you're using git, please make a branch for each separate contribution. We can cherry pick your commits, but pulling from a branch is easier. +* If you're submitting patches, please cut each fix or feature into a separate patch. +* There should be a Lighthouse[http://thoughtbot.lighthouseapp.com/projects/5807] ticket for any submission. If you've found a bug and want to fix it, open a new ticket at the same time. +* We've got github/lighthouse integration going, so you can update tickets when you commit. {This blog post}[http://hoth.entp.com/2008/4/11/github-and-lighthouse-sitting-in-a-tree] explains the commit options pretty well. +* Please don't send pull requests Just update the lighthouse ticket with the url for your fix (or attach the patch) when it's ready. The github pull requests pretty much get dropped on the floor until someone with commit rights notices them in the mailbox. +* Contributions without tests won't be accepted. The file /test/README explains the testing system pretty thoroughly. + + diff --git a/Rakefile b/Rakefile index 8c7cf29a..7f5f2f73 100644 --- a/Rakefile +++ b/Rakefile @@ -15,7 +15,7 @@ Rake::RDocTask.new { |rdoc| rdoc.title = "Shoulda -- Making tests easy on the fingers and eyes" rdoc.options << '--line-numbers' << '--inline-source' rdoc.template = "#{ENV['template']}.rb" if ENV['template'] - rdoc.rdoc_files.include('README', 'lib/**/*.rb') + rdoc.rdoc_files.include('**/*.rdoc', 'lib/**/*.rb') } desc 'Update documentation on website' From 5e4222694b8373c2a6f56afd9cef99c9cb223aa9 Mon Sep 17 00:00:00 2001 From: Tammer Saleh Date: Tue, 17 Jun 2008 13:07:10 -0400 Subject: [PATCH 3/8] fixed order of doc files --- Rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index 7f5f2f73..52c47ecf 100644 --- a/Rakefile +++ b/Rakefile @@ -15,7 +15,7 @@ Rake::RDocTask.new { |rdoc| rdoc.title = "Shoulda -- Making tests easy on the fingers and eyes" rdoc.options << '--line-numbers' << '--inline-source' rdoc.template = "#{ENV['template']}.rb" if ENV['template'] - rdoc.rdoc_files.include('**/*.rdoc', 'lib/**/*.rb') + rdoc.rdoc_files.include('README.rdoc', 'CONTRIBUTION_GUIDELINES.rdoc', 'lib/**/*.rb') } desc 'Update documentation on website' From fc938bb18557c1f5a801e3e2a15c9e4e9f8f03db Mon Sep 17 00:00:00 2001 From: Tammer Saleh Date: Sun, 22 Jun 2008 11:40:10 -0400 Subject: [PATCH 4/8] Removed ThoughtBot module. The ThoughtBot module had been added (by me) as a paranoid namespacing measure, just in case any other code wanted to use a class or module named Shoulda. Silly. --- README.rdoc | 8 +- lib/shoulda.rb | 8 +- lib/shoulda/active_record_helpers.rb | 1082 ++++++++--------- lib/shoulda/color.rb | 18 +- .../controller_tests/controller_tests.rb | 863 +++++++------ lib/shoulda/controller_tests/formats/html.rb | 338 +++-- lib/shoulda/controller_tests/formats/xml.rb | 328 +++-- lib/shoulda/gem/shoulda.rb | 428 ++++--- lib/shoulda/general.rb | 226 ++-- lib/shoulda/private_helpers.rb | 34 +- test/other/private_helpers_test.rb | 2 +- 11 files changed, 1660 insertions(+), 1675 deletions(-) diff --git a/README.rdoc b/README.rdoc index 13f7a008..80f8171d 100644 --- a/README.rdoc +++ b/README.rdoc @@ -10,7 +10,7 @@ Assertions:: Many common rails testing idioms have been distilled into a set of = Usage -=== Context Helpers (ThoughtBot::Shoulda::Context) +=== Context Helpers (Shoulda::Context) Stop killing your fingers with all of those underscores... Name your tests with plain sentences! @@ -43,7 +43,7 @@ Produces the following test methods: So readable! -=== ActiveRecord Tests (ThoughtBot::Shoulda::ActiveRecord) +=== ActiveRecord Tests (Shoulda::ActiveRecord) Quick macro tests for your ActiveRecord associations and validations: @@ -73,7 +73,7 @@ Quick macro tests for your ActiveRecord associations and validations: Makes TDD so much easier. -=== Controller Tests (ThoughtBot::Shoulda::Controller::ClassMethods) +=== Controller Tests (Shoulda::Controller::ClassMethods) Macros to test the most common controller patterns... @@ -105,7 +105,7 @@ Test entire controllers in a few lines... should_be_restful generates 40 tests on the fly, for both html and xml requests. -=== Helpful Assertions (ThoughtBot::Shoulda::General) +=== Helpful Assertions (Shoulda::General) More to come here, but have fun with what's there. diff --git a/lib/shoulda.rb b/lib/shoulda.rb index a90ff183..cd571df2 100644 --- a/lib/shoulda.rb +++ b/lib/shoulda.rb @@ -26,10 +26,10 @@ module Test # :nodoc: all module Unit class TestCase - include ThoughtBot::Shoulda::General - include ThoughtBot::Shoulda::Controller + include Shoulda::General + include Shoulda::Controller - extend ThoughtBot::Shoulda::ActiveRecord + extend Shoulda::ActiveRecord end end end @@ -37,7 +37,7 @@ end module ActionController #:nodoc: all module Integration class Session - include ThoughtBot::Shoulda::General + include Shoulda::General end end end diff --git a/lib/shoulda/active_record_helpers.rb b/lib/shoulda/active_record_helpers.rb index ec840571..15639864 100644 --- a/lib/shoulda/active_record_helpers.rb +++ b/lib/shoulda/active_record_helpers.rb @@ -1,604 +1,602 @@ -module ThoughtBot # :nodoc: - module Shoulda # :nodoc: - # = Macro test helpers for your active record models +module Shoulda # :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_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. # - # These helpers will test most of the validations and associations for your ActiveRecord models. + # Options: + # * :message - value the test expects to find in errors.on(:attribute). + # Regexp or string. Default = /blank/ # - # 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 + # Example: + # should_require_attributes :name, :phone_number # - # 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. - # - # Options: - # * :message - value the test expects to find in errors.on(:attribute). - # Regexp or string. Default = /blank/ - # - # Example: - # should_require_attributes :name, :phone_number - # - def should_require_attributes(*attributes) - message = get_options!(attributes, :message) - message ||= /blank/ - klass = model_class - - attributes.each do |attribute| - should "require #{attribute} to be set" do - object = klass.new - object.send("#{attribute}=", nil) - assert !object.valid?, "#{klass.name} does not require #{attribute}." - assert object.errors.on(attribute), "#{klass.name} does not require #{attribute}." - assert_contains(object.errors.on(attribute), message) - end + def should_require_attributes(*attributes) + message = get_options!(attributes, :message) + message ||= /blank/ + klass = model_class + + attributes.each do |attribute| + should "require #{attribute} to be set" do + object = klass.new + object.send("#{attribute}=", nil) + assert !object.valid?, "#{klass.name} does not require #{attribute}." + assert object.errors.on(attribute), "#{klass.name} does not require #{attribute}." + assert_contains(object.errors.on(attribute), message) end end + 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] - # - def should_require_unique_attributes(*attributes) - message, scope = get_options!(attributes, :message, :scoped_to) - scope = [*scope].compact - message ||= /taken/ - - klass = model_class - attributes.each do |attribute| - attribute = attribute.to_sym - should "require unique value for #{attribute}#{" scoped to #{scope.join(', ')}" if scope}" do - assert existing = klass.find(:first), "Can't find first #{klass}" - object = klass.new - - object.send(:"#{attribute}=", existing.send(attribute)) - if !scope.blank? - scope.each do |s| - assert_respond_to object, :"#{s}=", "#{klass.name} doesn't seem to have a #{s} attribute." - object.send(:"#{s}=", existing.send(s)) - 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] + # + def should_require_unique_attributes(*attributes) + message, scope = get_options!(attributes, :message, :scoped_to) + scope = [*scope].compact + message ||= /taken/ + + klass = model_class + attributes.each do |attribute| + attribute = attribute.to_sym + should "require unique value for #{attribute}#{" scoped to #{scope.join(', ')}" if scope}" do + assert existing = klass.find(:first), "Can't find first #{klass}" + object = klass.new + + object.send(:"#{attribute}=", existing.send(attribute)) + if !scope.blank? + scope.each do |s| + assert_respond_to object, :"#{s}=", "#{klass.name} doesn't seem to have a #{s} attribute." + object.send(:"#{s}=", existing.send(s)) + end + end + + assert !object.valid?, "#{klass.name} does not require a unique value for #{attribute}." + assert object.errors.on(attribute), "#{klass.name} does not require a unique value for #{attribute}." + + assert_contains(object.errors.on(attribute), message) + + # Now test that the object is valid when changing the scoped attribute + # TODO: There is a chance that we could change the scoped field + # to a value that's already taken. An alternative implementation + # could actually find all values for scope and create a unique + # one. + if !scope.blank? + scope.each do |s| + # Assume the scope is a foreign key if the field is nil + object.send(:"#{s}=", existing.send(s).nil? ? 1 : existing.send(s).next) end - - assert !object.valid?, "#{klass.name} does not require a unique value for #{attribute}." - assert object.errors.on(attribute), "#{klass.name} does not require a unique value for #{attribute}." - - assert_contains(object.errors.on(attribute), message) - - # Now test that the object is valid when changing the scoped attribute - # TODO: There is a chance that we could change the scoped field - # to a value that's already taken. An alternative implementation - # could actually find all values for scope and create a unique - # one. - if !scope.blank? - scope.each do |s| - # Assume the scope is a foreign key if the field is nil - object.send(:"#{s}=", existing.send(s).nil? ? 1 : existing.send(s).next) - end - object.errors.clear - object.valid? - scope.each do |s| - assert_does_not_contain(object.errors.on(attribute), message, - "after :#{s} set to #{object.send(s.to_sym)}") - end + object.errors.clear + object.valid? + scope.each do |s| + assert_does_not_contain(object.errors.on(attribute), message, + "after :#{s} set to #{object.send(s.to_sym)}") end end end end + end - # Ensures that the attribute cannot be set on mass update. - # Requires an existing record. - # - # should_protect_attributes :password, :admin_flag - # - def should_protect_attributes(*attributes) - get_options!(attributes) - klass = model_class + # Ensures that the attribute cannot be set on mass update. + # Requires an existing record. + # + # should_protect_attributes :password, :admin_flag + # + def should_protect_attributes(*attributes) + get_options!(attributes) + klass = model_class - attributes.each do |attribute| - attribute = attribute.to_sym - should "protect #{attribute} from mass updates" do - protected = klass.protected_attributes || [] - accessible = klass.accessible_attributes || [] + attributes.each do |attribute| + attribute = attribute.to_sym + should "protect #{attribute} from mass updates" do + protected = klass.protected_attributes || [] + accessible = klass.accessible_attributes || [] - assert protected.include?(attribute.to_s) || !accessible.include?(attribute.to_s), - (accessible.empty? ? - "#{klass} is protecting #{protected.to_a.to_sentence}, but not #{attribute}." : - "#{klass} has made #{attribute} accessible") - end + assert protected.include?(attribute.to_s) || !accessible.include?(attribute.to_s), + (accessible.empty? ? + "#{klass} is protecting #{protected.to_a.to_sentence}, but not #{attribute}." : + "#{klass} has made #{attribute} accessible") + end + end + 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" + # + def should_not_allow_values_for(attribute, *bad_values) + message = get_options!(bad_values, :message) + message ||= /invalid/ + klass = model_class + bad_values.each do |v| + should "not allow #{attribute} to be set to #{v.inspect}" 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), message, "when set to \"#{v}\"") + end + end + end + + # 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" + # + def should_allow_values_for(attribute, *good_values) + get_options!(good_values) + klass = model_class + good_values.each do |v| + should "allow #{attribute} to be set to #{v.inspect}" do + assert object = klass.find(:first), "Can't find first #{klass}" + object.send("#{attribute}=", v) + object.save + assert_nil object.errors.on(attribute) + end + end + 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) + # + def should_ensure_length_in_range(attribute, range, opts = {}) + short_message, long_message = get_options!([opts], :short_message, :long_message) + short_message ||= /short/ + long_message ||= /long/ + + klass = model_class + min_length = range.first + max_length = range.last + same_length = (min_length == max_length) + + if min_length > 0 + should "not allow #{attribute} to be less than #{min_length} chars long" do + min_value = "x" * (min_length - 1) + 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), short_message, "when set to \"#{min_value}\"") + end + end + + if min_length > 0 + should "allow #{attribute} to be exactly #{min_length} chars long" do + min_value = "x" * min_length + assert object = klass.find(:first), "Can't find first #{klass}" + object.send("#{attribute}=", min_value) + object.save + assert_does_not_contain(object.errors.on(attribute), short_message, "when set to \"#{min_value}\"") end 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" - # - def should_not_allow_values_for(attribute, *bad_values) - message = get_options!(bad_values, :message) - message ||= /invalid/ - klass = model_class - bad_values.each do |v| - should "not allow #{attribute} to be set to #{v.inspect}" 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), message, "when set to \"#{v}\"") - end - end - end - - # 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" - # - def should_allow_values_for(attribute, *good_values) - get_options!(good_values) - klass = model_class - good_values.each do |v| - should "allow #{attribute} to be set to #{v.inspect}" do - assert object = klass.find(:first), "Can't find first #{klass}" - object.send("#{attribute}=", v) - object.save - assert_nil object.errors.on(attribute) - end - end + should "not allow #{attribute} to be more than #{max_length} chars long" do + max_value = "x" * (max_length + 1) + 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), long_message, "when set to \"#{max_value}\"") 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) - # - def should_ensure_length_in_range(attribute, range, opts = {}) - short_message, long_message = get_options!([opts], :short_message, :long_message) - short_message ||= /short/ - long_message ||= /long/ - - klass = model_class - min_length = range.first - max_length = range.last - same_length = (min_length == max_length) - - if min_length > 0 - should "not allow #{attribute} to be less than #{min_length} chars long" do - min_value = "x" * (min_length - 1) - 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), short_message, "when set to \"#{min_value}\"") - end - end - - if min_length > 0 - should "allow #{attribute} to be exactly #{min_length} chars long" do - min_value = "x" * min_length - assert object = klass.find(:first), "Can't find first #{klass}" - object.send("#{attribute}=", min_value) - object.save - assert_does_not_contain(object.errors.on(attribute), short_message, "when set to \"#{min_value}\"") - end - end - - should "not allow #{attribute} to be more than #{max_length} chars long" do - max_value = "x" * (max_length + 1) + unless same_length + should "allow #{attribute} to be exactly #{max_length} chars long" do + max_value = "x" * max_length 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), long_message, "when set to \"#{max_value}\"") - end - - unless same_length - should "allow #{attribute} to be exactly #{max_length} chars long" do - max_value = "x" * max_length - assert object = klass.find(:first), "Can't find first #{klass}" - object.send("#{attribute}=", max_value) - object.save - assert_does_not_contain(object.errors.on(attribute), long_message, "when set to \"#{max_value}\"") - end - end - 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 - # - def should_ensure_length_at_least(attribute, min_length, opts = {}) - short_message = get_options!([opts], :short_message) - short_message ||= /short/ - - klass = model_class - - if min_length > 0 - min_value = "x" * (min_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), short_message, "when set to \"#{min_value}\"") - end - end - should "allow #{attribute} to be at least #{min_length} chars long" do - valid_value = "x" * (min_length) - assert object = klass.find(:first), "Can't find first #{klass}" - object.send("#{attribute}=", valid_value) - assert object.save, "Could not save #{klass} with #{attribute} set to \"#{valid_value}\"" - end - 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) - # - def should_ensure_value_in_range(attribute, range, opts = {}) - low_message, high_message = get_options!([opts], :low_message, :high_message) - low_message ||= /included/ - 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), low_message, "when set to \"#{v}\"") - end - - should "allow #{attribute} to be #{min}" do - v = min - assert object = klass.find(:first), "Can't find first #{klass}" - object.send("#{attribute}=", v) object.save - assert_does_not_contain(object.errors.on(attribute), low_message, "when set to \"#{v}\"") + assert_does_not_contain(object.errors.on(attribute), long_message, "when set to \"#{max_value}\"") end - - should "not allow #{attribute} to be more than #{max}" do - v = max + 1 + end + 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 + # + def should_ensure_length_at_least(attribute, min_length, opts = {}) + short_message = get_options!([opts], :short_message) + short_message ||= /short/ + + klass = model_class + + if min_length > 0 + min_value = "x" * (min_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}=", 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), high_message, "when set to \"#{v}\"") + 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), short_message, "when set to \"#{min_value}\"") end + end + should "allow #{attribute} to be at least #{min_length} chars long" do + valid_value = "x" * (min_length) + assert object = klass.find(:first), "Can't find first #{klass}" + object.send("#{attribute}=", valid_value) + assert object.save, "Could not save #{klass} with #{attribute} set to \"#{valid_value}\"" + end + end - should "allow #{attribute} to be #{max}" do - v = max - assert object = klass.find(:first), "Can't find first #{klass}" - object.send("#{attribute}=", v) - object.save - assert_does_not_contain(object.errors.on(attribute), high_message, "when set to \"#{v}\"") - end - 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) + # + def should_ensure_value_in_range(attribute, range, opts = {}) + low_message, high_message = get_options!([opts], :low_message, :high_message) + low_message ||= /included/ + high_message ||= /included/ - # 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 - # - def should_only_allow_numeric_values_for(*attributes) - message = get_options!(attributes, :message) - 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), message) - end - end + 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), low_message, "when set to \"#{v}\"") 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| - name = "have many #{association}" - name += " through #{through}" if through - name += " dependent => #{dependent}" if dependent - should name do - reflection = klass.reflect_on_association(association) - assert reflection, "#{klass.name} does not have any relationship to #{association}" - assert_equal :has_many, reflection.macro - - associated_klass = (reflection.options[:class_name] || association.to_s.classify).constantize - - if through - through_reflection = klass.reflect_on_association(through) - assert through_reflection, "#{klass.name} does not have any relationship to #{through}" - assert_equal(through, reflection.options[:through]) - end - - if dependent - assert_equal dependent.to_s, - reflection.options[:dependent].to_s, - "#{associated_klass.name} should have #{dependent} dependency" - end - - # Check for the existence of the foreign key on the other table - unless reflection.options[:through] - if reflection.options[:foreign_key] - fk = reflection.options[:foreign_key] - elsif reflection.options[:as] - fk = reflection.options[:as].to_s.foreign_key - else - fk = reflection.primary_key_name - end - - assert associated_klass.column_names.include?(fk.to_s), - "#{associated_klass.name} does not have a #{fk} foreign key." - end - end - end + should "allow #{attribute} to be #{min}" do + v = min + assert object = klass.find(:first), "Can't find first #{klass}" + object.send("#{attribute}=", v) + object.save + assert_does_not_contain(object.errors.on(attribute), low_message, "when set to \"#{v}\"") 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 - # - def should_have_one(*associations) - get_options!(associations) - klass = model_class - associations.each do |association| - should "have one #{association}" do - reflection = klass.reflect_on_association(association) - assert reflection, "#{klass.name} does not have any relationship to #{association}" - assert_equal :has_one, reflection.macro - - associated_klass = (reflection.options[:class_name] || association.to_s.camelize).constantize + 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), high_message, "when set to \"#{v}\"") + end + should "allow #{attribute} to be #{max}" do + v = max + assert object = klass.find(:first), "Can't find first #{klass}" + object.send("#{attribute}=", v) + object.save + assert_does_not_contain(object.errors.on(attribute), high_message, "when set to \"#{v}\"") + end + 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 + # + def should_only_allow_numeric_values_for(*attributes) + message = get_options!(attributes, :message) + 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), message) + end + end + 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| + name = "have many #{association}" + name += " through #{through}" if through + name += " dependent => #{dependent}" if dependent + should name do + reflection = klass.reflect_on_association(association) + assert reflection, "#{klass.name} does not have any relationship to #{association}" + assert_equal :has_many, reflection.macro + + associated_klass = (reflection.options[:class_name] || association.to_s.classify).constantize + + if through + through_reflection = klass.reflect_on_association(through) + assert through_reflection, "#{klass.name} does not have any relationship to #{through}" + assert_equal(through, reflection.options[:through]) + end + + if dependent + assert_equal dependent.to_s, + reflection.options[:dependent].to_s, + "#{associated_klass.name} should have #{dependent} dependency" + end + + # Check for the existence of the foreign key on the other table + unless reflection.options[:through] if reflection.options[:foreign_key] fk = reflection.options[:foreign_key] elsif reflection.options[:as] fk = reflection.options[:as].to_s.foreign_key - fk_type = fk.gsub(/_id$/, '_type') - assert associated_klass.column_names.include?(fk_type), - "#{associated_klass.name} does not have a #{fk_type} column." else - fk = klass.name.foreign_key + fk = reflection.primary_key_name end - assert associated_klass.column_names.include?(fk.to_s), - "#{associated_klass.name} does not have a #{fk} foreign key." + + assert associated_klass.column_names.include?(fk.to_s), + "#{associated_klass.name} does not have a #{fk} foreign key." 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 + end - associations.each do |association| - should "should have and belong to many #{association}" do - reflection = klass.reflect_on_association(association) - assert reflection, "#{klass.name} does not have any relationship to #{association}" - assert_equal :has_and_belongs_to_many, reflection.macro - table = reflection.options[:join_table] - assert ::ActiveRecord::Base.connection.tables.include?(table), "table #{table} doesn't exist" - end - end - end - - # Ensure that the belongs_to relationship exists. - # - # should_belong_to :parent - # - def should_belong_to(*associations) - get_options!(associations) - klass = model_class - associations.each do |association| - should "belong_to #{association}" do - reflection = klass.reflect_on_association(association) - assert reflection, "#{klass.name} does not have any relationship to #{association}" - assert_equal :belongs_to, reflection.macro + # 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 + # + def should_have_one(*associations) + get_options!(associations) + klass = model_class + associations.each do |association| + should "have one #{association}" do + reflection = klass.reflect_on_association(association) + assert reflection, "#{klass.name} does not have any relationship to #{association}" + assert_equal :has_one, reflection.macro + + associated_klass = (reflection.options[:class_name] || association.to_s.camelize).constantize - unless reflection.options[:polymorphic] - associated_klass = (reflection.options[:class_name] || association.to_s.classify).constantize - fk = reflection.options[:foreign_key] || reflection.primary_key_name - assert klass.column_names.include?(fk.to_s), "#{klass.name} does not have a #{fk} foreign key." - end - 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}" + if reflection.options[:foreign_key] + fk = reflection.options[:foreign_key] + elsif reflection.options[:as] + fk = reflection.options[:as].to_s.foreign_key + fk_type = fk.gsub(/_id$/, '_type') + assert associated_klass.column_names.include?(fk_type), + "#{associated_klass.name} does not have a #{fk_type} column." + else + fk = klass.name.foreign_key end + assert associated_klass.column_names.include?(fk.to_s), + "#{associated_klass.name} does not have a #{fk} foreign key." 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}" + # 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| + should "should have and belong to many #{association}" do + reflection = klass.reflect_on_association(association) + assert reflection, "#{klass.name} does not have any relationship to #{association}" + assert_equal :has_and_belongs_to_many, reflection.macro + table = reflection.options[:join_table] + assert ::ActiveRecord::Base.connection.tables.include?(table), "table #{table} doesn't exist" + end + end + end + + # Ensure that the belongs_to relationship exists. + # + # should_belong_to :parent + # + def should_belong_to(*associations) + get_options!(associations) + klass = model_class + associations.each do |association| + should "belong_to #{association}" do + reflection = klass.reflect_on_association(association) + assert reflection, "#{klass.name} does not have any relationship to #{association}" + assert_equal :belongs_to, reflection.macro + + unless reflection.options[:polymorphic] + associated_klass = (reflection.options[:class_name] || association.to_s.classify).constantize + fk = reflection.options[:foreign_key] || reflection.primary_key_name + assert klass.column_names.include?(fk.to_s), "#{klass.name} does not have a #{fk} foreign key." end end end - - # Ensure that the given columns are defined on the models backing SQL table. - # - # should_have_db_columns :id, :email, :name, :created_at - # - def should_have_db_columns(*columns) - column_type = get_options!(columns, :type) - klass = model_class - columns.each do |name| - test_name = "have column #{name}" - test_name += " of type #{column_type}" if column_type - should test_name do - column = klass.columns.detect {|c| c.name == name.to_s } - assert column, "#{klass.name} does not have column #{name}" - 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 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)' - # - def should_have_db_column(name, opts = {}) - klass = model_class - test_name = "have column named :#{name}" - test_name += " with options " + opts.inspect unless opts.empty? + # 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. + # + # should_have_db_columns :id, :email, :name, :created_at + # + def should_have_db_columns(*columns) + column_type = get_options!(columns, :type) + klass = model_class + columns.each do |name| + test_name = "have column #{name}" + test_name += " of type #{column_type}" if column_type should test_name do column = klass.columns.detect {|c| c.name == name.to_s } assert column, "#{klass.name} does not have column #{name}" - opts.each do |k, v| - assert_equal column.instance_variable_get("@#{k}").to_s, v.to_s, ":#{name} column on table for #{klass} does not match option :#{k}" - end end end - - # 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 - # - def should_have_indices(*columns) - table = model_class.name.tableize - indices = ::ActiveRecord::Base.connection.indexes(table).map(&:columns) - - columns.each do |column| - should "have index on #{table} for #{column.inspect}" do - columns = [column].flatten.map(&:to_s) - assert_contains(indices, columns) - 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. - # - # 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 - # - def should_require_acceptance_of(*attributes) - message = get_options!(attributes, :message) - message ||= /must be accepted/ - klass = model_class - - attributes.each do |attribute| - should "require #{attribute} to be accepted" do - object = klass.new - object.send("#{attribute}=", false) - - assert !object.valid?, "#{klass.name} does not require acceptance of #{attribute}." - assert object.errors.on(attribute), "#{klass.name} does not require acceptance of #{attribute}." - assert_contains(object.errors.on(attribute), message) - end - end - end - - private - - include ThoughtBot::Shoulda::Private 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)' + # + def should_have_db_column(name, opts = {}) + klass = model_class + test_name = "have column named :#{name}" + test_name += " with options " + opts.inspect unless opts.empty? + should test_name do + column = klass.columns.detect {|c| c.name == name.to_s } + assert column, "#{klass.name} does not have column #{name}" + opts.each do |k, v| + assert_equal column.instance_variable_get("@#{k}").to_s, v.to_s, ":#{name} column on table for #{klass} does not match option :#{k}" + end + end + end + + # 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 + # + def should_have_indices(*columns) + table = model_class.name.tableize + indices = ::ActiveRecord::Base.connection.indexes(table).map(&:columns) + + columns.each do |column| + should "have index on #{table} for #{column.inspect}" do + columns = [column].flatten.map(&:to_s) + assert_contains(indices, columns) + 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. + # + # 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 + # + def should_require_acceptance_of(*attributes) + message = get_options!(attributes, :message) + message ||= /must be accepted/ + klass = model_class + + attributes.each do |attribute| + should "require #{attribute} to be accepted" do + object = klass.new + object.send("#{attribute}=", false) + + assert !object.valid?, "#{klass.name} does not require acceptance of #{attribute}." + assert object.errors.on(attribute), "#{klass.name} does not require acceptance of #{attribute}." + assert_contains(object.errors.on(attribute), message) + end + end + end + + private + + include Shoulda::Private end end diff --git a/lib/shoulda/color.rb b/lib/shoulda/color.rb index 1ccfad2b..1324dd7b 100644 --- a/lib/shoulda/color.rb +++ b/lib/shoulda/color.rb @@ -9,7 +9,7 @@ require 'test/unit/ui/console/testrunner' # every rake task, as though there was another (empty) set of tests. # A fix would be most welcome. # -module ThoughtBot::Shoulda::Color +module Shoulda::Color COLORS = { :clear => 0, :red => 31, :green => 32, :yellow => 33 } # :nodoc: def self.method_missing(color_name, *args) # :nodoc: color(color_name) + args.first + color(:clear) @@ -25,7 +25,7 @@ module Test # :nodoc: alias :old_to_s :to_s def to_s if old_to_s =~ /\d+ tests, \d+ assertions, (\d+) failures, (\d+) errors/ - ThoughtBot::Shoulda::Color.send($1.to_i != 0 || $2.to_i != 0 ? :red : :green, $&) + Shoulda::Color.send($1.to_i != 0 || $2.to_i != 0 ? :red : :green, $&) end end end @@ -43,16 +43,16 @@ module Test # :nodoc: class Failure # :nodoc: alias :old_long_display :long_display def long_display - # old_long_display.sub('Failure', ThoughtBot::Shoulda::Color.red('Failure')) - ThoughtBot::Shoulda::Color.red(old_long_display) + # old_long_display.sub('Failure', Shoulda::Color.red('Failure')) + Shoulda::Color.red(old_long_display) end end class Error # :nodoc: alias :old_long_display :long_display def long_display - # old_long_display.sub('Error', ThoughtBot::Shoulda::Color.yellow('Error')) - ThoughtBot::Shoulda::Color.yellow(old_long_display) + # old_long_display.sub('Error', Shoulda::Color.yellow('Error')) + Shoulda::Color.yellow(old_long_display) end end @@ -62,9 +62,9 @@ module Test # :nodoc: def output_single(something, level=NORMAL) return unless (output?(level)) something = case something - when '.' then ThoughtBot::Shoulda::Color.green('.') - when 'F' then ThoughtBot::Shoulda::Color.red("F") - when 'E' then ThoughtBot::Shoulda::Color.yellow("E") + when '.' then Shoulda::Color.green('.') + when 'F' then Shoulda::Color.red("F") + when 'E' then Shoulda::Color.yellow("E") else something end @io.write(something) diff --git a/lib/shoulda/controller_tests/controller_tests.rb b/lib/shoulda/controller_tests/controller_tests.rb index f0059c0f..c5b1d5d2 100644 --- a/lib/shoulda/controller_tests/controller_tests.rb +++ b/lib/shoulda/controller_tests/controller_tests.rb @@ -1,467 +1,464 @@ -module ThoughtBot # :nodoc: - module Shoulda # :nodoc: - module Controller - def self.included(other) # :nodoc: - other.class_eval do - extend ThoughtBot::Shoulda::Controller::ClassMethods - include ThoughtBot::Shoulda::Controller::InstanceMethods - ThoughtBot::Shoulda::Controller::ClassMethods::VALID_FORMATS.each do |format| - include "ThoughtBot::Shoulda::Controller::#{format.to_s.upcase}".constantize - end +module Shoulda # :nodoc: + module Controller + def self.included(other) # :nodoc: + other.class_eval do + extend Shoulda::Controller::ClassMethods + include Shoulda::Controller::InstanceMethods + Shoulda::Controller::ClassMethods::VALID_FORMATS.each do |format| + include "Shoulda::Controller::#{format.to_s.upcase}".constantize end end - - # = Macro test helpers for your controllers - # - # By using the macro helpers you can quickly and easily create concise and easy to read test suites. + end + + # = Macro test helpers for your controllers + # + # By using the macro helpers you can quickly and easily create concise and easy to read test suites. + # + # This code segment: + # context "on GET to :show for first record" do + # setup do + # get :show, :id => 1 + # end + # + # should_assign_to :user + # should_respond_with :success + # should_render_template :show + # should_not_set_the_flash + # + # should "do something else really cool" do + # assert_equal 1, assigns(:user).id + # end + # end + # + # Would produce 5 tests for the +show+ action + # + # Furthermore, the should_be_restful helper will create an entire set of tests which will verify that your + # controller responds restfully to a variety of requested formats. + module ClassMethods + # Formats tested by #should_be_restful. Defaults to [:html, :xml] + VALID_FORMATS = Dir.glob(File.join(File.dirname(__FILE__), 'formats', '*.rb')).map { |f| File.basename(f, '.rb') }.map(&:to_sym) # :doc: + VALID_FORMATS.each {|f| require "shoulda/controller_tests/formats/#{f}.rb"} + + # Actions tested by #should_be_restful + VALID_ACTIONS = [:index, :show, :new, :edit, :create, :update, :destroy] # :doc: + + # A ResourceOptions object is passed into should_be_restful in order to configure the tests for your controller. # - # This code segment: - # context "on GET to :show for first record" do - # setup do - # get :show, :id => 1 + # Example: + # class UsersControllerTest < Test::Unit::TestCase + # load_all_fixtures + # + # def setup + # ...normal setup code... + # @user = User.find(:first) # end # - # should_assign_to :user - # should_respond_with :success - # should_render_template :show - # should_not_set_the_flash - # - # should "do something else really cool" do - # assert_equal 1, assigns(:user).id + # should_be_restful do |resource| + # resource.identifier = :id + # resource.klass = User + # resource.object = :user + # resource.parent = [] + # resource.actions = [:index, :show, :new, :edit, :update, :create, :destroy] + # resource.formats = [:html, :xml] + # + # resource.create.params = { :name => "bob", :email => 'bob@bob.com', :age => 13} + # resource.update.params = { :name => "sue" } + # + # resource.create.redirect = "user_url(@user)" + # resource.update.redirect = "user_url(@user)" + # resource.destroy.redirect = "users_url" + # + # resource.create.flash = /created/i + # resource.update.flash = /updated/i + # resource.destroy.flash = /removed/i # end # end # - # Would produce 5 tests for the +show+ action + # Whenever possible, the resource attributes will be set to sensible defaults. # - # Furthermore, the should_be_restful helper will create an entire set of tests which will verify that your - # controller responds restfully to a variety of requested formats. - module ClassMethods - # Formats tested by #should_be_restful. Defaults to [:html, :xml] - VALID_FORMATS = Dir.glob(File.join(File.dirname(__FILE__), 'formats', '*.rb')).map { |f| File.basename(f, '.rb') }.map(&:to_sym) # :doc: - VALID_FORMATS.each {|f| require "shoulda/controller_tests/formats/#{f}.rb"} + class ResourceOptions + # Configuration options for the create, update, destroy actions under should_be_restful + class ActionOptions + # String evaled to get the target of the redirection. + # All of the instance variables set by the controller will be available to the + # evaled code. + # + # Example: + # resource.create.redirect = "user_url(@user.company, @user)" + # + # Defaults to a generated url based on the name of the controller, the action, and the resource.parents list. + attr_accessor :redirect - # Actions tested by #should_be_restful - VALID_ACTIONS = [:index, :show, :new, :edit, :create, :update, :destroy] # :doc: + # String or Regexp describing a value expected in the flash. Will match against any flash key. + # + # Defaults: + # destroy:: /removed/ + # create:: /created/ + # update:: /updated/ + attr_accessor :flash + + # Hash describing the params that should be sent in with this action. + attr_accessor :params + end - # A ResourceOptions object is passed into should_be_restful in order to configure the tests for your controller. - # + # Configuration options for the denied actions under should_be_restful + # # Example: - # class UsersControllerTest < Test::Unit::TestCase - # load_all_fixtures - # - # def setup - # ...normal setup code... - # @user = User.find(:first) + # context "The public" do + # setup do + # @request.session[:logged_in] = false # end # # should_be_restful do |resource| - # resource.identifier = :id - # resource.klass = User - # resource.object = :user - # resource.parent = [] - # resource.actions = [:index, :show, :new, :edit, :update, :create, :destroy] - # resource.formats = [:html, :xml] + # resource.parent = :user # - # resource.create.params = { :name => "bob", :email => 'bob@bob.com', :age => 13} - # resource.update.params = { :name => "sue" } - # - # resource.create.redirect = "user_url(@user)" - # resource.update.redirect = "user_url(@user)" - # resource.destroy.redirect = "users_url" - # - # resource.create.flash = /created/i - # resource.update.flash = /updated/i - # resource.destroy.flash = /removed/i + # resource.denied.actions = [:index, :show, :edit, :new, :create, :update, :destroy] + # resource.denied.flash = /get outta here/i + # resource.denied.redirect = 'new_session_url' + # end + # end + # + class DeniedOptions + # String evaled to get the target of the redirection. + # All of the instance variables set by the controller will be available to the + # evaled code. + # + # Example: + # resource.create.redirect = "user_url(@user.company, @user)" + attr_accessor :redirect + + # String or Regexp describing a value expected in the flash. Will match against any flash key. + # + # Example: + # resource.create.flash = /created/ + attr_accessor :flash + + # Actions that should be denied (only used by resource.denied). Note that these actions will + # only be tested if they are also listed in +resource.actions+ + # The special value of :all will deny all of the REST actions. + attr_accessor :actions + end + + # Name of key in params that references the primary key. + # Will almost always be :id (default), unless you are using a plugin or have patched rails. + attr_accessor :identifier + + # Name of the ActiveRecord class this resource is responsible for. Automatically determined from + # test class if not explicitly set. UserTest => "User" + attr_accessor :klass + + # Name of the instantiated ActiveRecord object that should be used by some of the tests. + # Defaults to the underscored name of the AR class. CompanyManager => :company_manager + attr_accessor :object + + # Name of the parent AR objects. Can be set as parent= or parents=, and can take either + # the name of the parent resource (if there's only one), or an array of names (if there's + # more than one). + # + # Example: + # # in the routes... + # map.resources :companies do + # map.resources :people do + # map.resources :limbs # end # end # - # Whenever possible, the resource attributes will be set to sensible defaults. - # - class ResourceOptions - # Configuration options for the create, update, destroy actions under should_be_restful - class ActionOptions - # String evaled to get the target of the redirection. - # All of the instance variables set by the controller will be available to the - # evaled code. - # - # Example: - # resource.create.redirect = "user_url(@user.company, @user)" - # - # Defaults to a generated url based on the name of the controller, the action, and the resource.parents list. - attr_accessor :redirect - - # String or Regexp describing a value expected in the flash. Will match against any flash key. - # - # Defaults: - # destroy:: /removed/ - # create:: /created/ - # update:: /updated/ - attr_accessor :flash - - # Hash describing the params that should be sent in with this action. - attr_accessor :params - end - - # Configuration options for the denied actions under should_be_restful - # - # Example: - # context "The public" do - # setup do - # @request.session[:logged_in] = false - # end - # - # should_be_restful do |resource| - # resource.parent = :user - # - # resource.denied.actions = [:index, :show, :edit, :new, :create, :update, :destroy] - # resource.denied.flash = /get outta here/i - # resource.denied.redirect = 'new_session_url' - # end - # end - # - class DeniedOptions - # String evaled to get the target of the redirection. - # All of the instance variables set by the controller will be available to the - # evaled code. - # - # Example: - # resource.create.redirect = "user_url(@user.company, @user)" - attr_accessor :redirect - - # String or Regexp describing a value expected in the flash. Will match against any flash key. - # - # Example: - # resource.create.flash = /created/ - attr_accessor :flash - - # Actions that should be denied (only used by resource.denied). Note that these actions will - # only be tested if they are also listed in +resource.actions+ - # The special value of :all will deny all of the REST actions. - attr_accessor :actions - end - - # Name of key in params that references the primary key. - # Will almost always be :id (default), unless you are using a plugin or have patched rails. - attr_accessor :identifier - - # Name of the ActiveRecord class this resource is responsible for. Automatically determined from - # test class if not explicitly set. UserTest => "User" - attr_accessor :klass - - # Name of the instantiated ActiveRecord object that should be used by some of the tests. - # Defaults to the underscored name of the AR class. CompanyManager => :company_manager - attr_accessor :object - - # Name of the parent AR objects. Can be set as parent= or parents=, and can take either - # the name of the parent resource (if there's only one), or an array of names (if there's - # more than one). - # - # Example: - # # in the routes... - # map.resources :companies do - # map.resources :people do - # map.resources :limbs - # end - # end - # - # # in the tests... - # class PeopleControllerTest < Test::Unit::TestCase - # should_be_restful do |resource| - # resource.parent = :companies - # end - # end - # - # class LimbsControllerTest < Test::Unit::TestCase - # should_be_restful do |resource| - # resource.parents = [:companies, :people] - # end - # end - attr_accessor :parent - alias parents parent - alias parents= parent= - - # Actions that should be tested. Must be a subset of VALID_ACTIONS (default). - # Tests for each actionw will only be generated if the action is listed here. - # The special value of :all will test all of the REST actions. - # - # Example (for a read-only controller): - # resource.actions = [:show, :index] - attr_accessor :actions - - # Formats that should be tested. Must be a subset of VALID_FORMATS (default). - # Each action will be tested against the formats listed here. The special value - # of :all will test all of the supported formats. - # - # Example: - # resource.actions = [:html, :xml] - attr_accessor :formats - - # ActionOptions object specifying options for the create action. - attr_accessor :create - - # ActionOptions object specifying options for the update action. - attr_accessor :update - - # ActionOptions object specifying options for the desrtoy action. - attr_accessor :destroy - - # DeniedOptions object specifying which actions should return deny a request, and what should happen in that case. - attr_accessor :denied - - def initialize # :nodoc: - @create = ActionOptions.new - @update = ActionOptions.new - @destroy = ActionOptions.new - @denied = DeniedOptions.new - - @create.flash ||= /created/i - @update.flash ||= /updated/i - @destroy.flash ||= /removed/i - @denied.flash ||= /denied/i - - @create.params ||= {} - @update.params ||= {} - - @actions = VALID_ACTIONS - @formats = VALID_FORMATS - @denied.actions = [] - end - - def normalize!(target) # :nodoc: - @denied.actions = VALID_ACTIONS if @denied.actions == :all - @actions = VALID_ACTIONS if @actions == :all - @formats = VALID_FORMATS if @formats == :all - - @denied.actions = @denied.actions.map(&:to_sym) - @actions = @actions.map(&:to_sym) - @formats = @formats.map(&:to_sym) - - ensure_valid_members(@actions, VALID_ACTIONS, 'actions') - ensure_valid_members(@denied.actions, VALID_ACTIONS, 'denied.actions') - ensure_valid_members(@formats, VALID_FORMATS, 'formats') - - @identifier ||= :id - @klass ||= target.name.gsub(/ControllerTest$/, '').singularize.constantize - @object ||= @klass.name.tableize.singularize - @parent ||= [] - @parent = [@parent] unless @parent.is_a? Array - - collection_helper = [@parent, @object.to_s.pluralize, 'url'].flatten.join('_') - collection_args = @parent.map {|n| "@#{object}.#{n}"}.join(', ') - @destroy.redirect ||= "#{collection_helper}(#{collection_args})" - - member_helper = [@parent, @object, 'url'].flatten.join('_') - member_args = [@parent.map {|n| "@#{object}.#{n}"}, "@#{object}"].flatten.join(', ') - @create.redirect ||= "#{member_helper}(#{member_args})" - @update.redirect ||= "#{member_helper}(#{member_args})" - @denied.redirect ||= "new_session_url" - end - - private - - def ensure_valid_members(ary, valid_members, name) # :nodoc: - invalid = ary - valid_members - raise ArgumentError, "Unsupported #{name}: #{invalid.inspect}" unless invalid.empty? - end - end - - # :section: should_be_restful - # Generates a full suite of tests for a restful controller. - # - # The following definition will generate tests for the +index+, +show+, +new+, - # +edit+, +create+, +update+ and +destroy+ actions, in both +html+ and +xml+ formats: - # - # should_be_restful do |resource| - # resource.parent = :user - # - # resource.create.params = { :title => "first post", :body => 'blah blah blah'} - # resource.update.params = { :title => "changed" } + # # in the tests... + # class PeopleControllerTest < Test::Unit::TestCase + # should_be_restful do |resource| + # resource.parent = :companies + # end # end # - # This generates about 40 tests, all of the format: - # "on GET to :show should assign @user." - # "on GET to :show should not set the flash." - # "on GET to :show should render 'show' template." - # "on GET to :show should respond with success." - # "on GET to :show as xml should assign @user." - # "on GET to :show as xml should have ContentType set to 'application/xml'." - # "on GET to :show as xml should respond with success." - # "on GET to :show as xml should return as the root element." - # The +resource+ parameter passed into the block is a ResourceOptions object, and - # is used to configure the tests for the details of your resources. + # class LimbsControllerTest < Test::Unit::TestCase + # should_be_restful do |resource| + # resource.parents = [:companies, :people] + # end + # end + attr_accessor :parent + alias parents parent + alias parents= parent= + + # Actions that should be tested. Must be a subset of VALID_ACTIONS (default). + # Tests for each actionw will only be generated if the action is listed here. + # The special value of :all will test all of the REST actions. # - def should_be_restful(&blk) # :yields: resource - resource = ResourceOptions.new - blk.call(resource) - resource.normalize!(self) + # Example (for a read-only controller): + # resource.actions = [:show, :index] + attr_accessor :actions - resource.formats.each do |format| - resource.actions.each do |action| - if self.respond_to? :"make_#{action}_#{format}_tests" - self.send(:"make_#{action}_#{format}_tests", resource) - else - should "test #{action} #{format}" do - flunk "Test for #{action} as #{format} not implemented" - end + # Formats that should be tested. Must be a subset of VALID_FORMATS (default). + # Each action will be tested against the formats listed here. The special value + # of :all will test all of the supported formats. + # + # Example: + # resource.actions = [:html, :xml] + attr_accessor :formats + + # ActionOptions object specifying options for the create action. + attr_accessor :create + + # ActionOptions object specifying options for the update action. + attr_accessor :update + + # ActionOptions object specifying options for the desrtoy action. + attr_accessor :destroy + + # DeniedOptions object specifying which actions should return deny a request, and what should happen in that case. + attr_accessor :denied + + def initialize # :nodoc: + @create = ActionOptions.new + @update = ActionOptions.new + @destroy = ActionOptions.new + @denied = DeniedOptions.new + + @create.flash ||= /created/i + @update.flash ||= /updated/i + @destroy.flash ||= /removed/i + @denied.flash ||= /denied/i + + @create.params ||= {} + @update.params ||= {} + + @actions = VALID_ACTIONS + @formats = VALID_FORMATS + @denied.actions = [] + end + + def normalize!(target) # :nodoc: + @denied.actions = VALID_ACTIONS if @denied.actions == :all + @actions = VALID_ACTIONS if @actions == :all + @formats = VALID_FORMATS if @formats == :all + + @denied.actions = @denied.actions.map(&:to_sym) + @actions = @actions.map(&:to_sym) + @formats = @formats.map(&:to_sym) + + ensure_valid_members(@actions, VALID_ACTIONS, 'actions') + ensure_valid_members(@denied.actions, VALID_ACTIONS, 'denied.actions') + ensure_valid_members(@formats, VALID_FORMATS, 'formats') + + @identifier ||= :id + @klass ||= target.name.gsub(/ControllerTest$/, '').singularize.constantize + @object ||= @klass.name.tableize.singularize + @parent ||= [] + @parent = [@parent] unless @parent.is_a? Array + + collection_helper = [@parent, @object.to_s.pluralize, 'url'].flatten.join('_') + collection_args = @parent.map {|n| "@#{object}.#{n}"}.join(', ') + @destroy.redirect ||= "#{collection_helper}(#{collection_args})" + + member_helper = [@parent, @object, 'url'].flatten.join('_') + member_args = [@parent.map {|n| "@#{object}.#{n}"}, "@#{object}"].flatten.join(', ') + @create.redirect ||= "#{member_helper}(#{member_args})" + @update.redirect ||= "#{member_helper}(#{member_args})" + @denied.redirect ||= "new_session_url" + end + + private + + def ensure_valid_members(ary, valid_members, name) # :nodoc: + invalid = ary - valid_members + raise ArgumentError, "Unsupported #{name}: #{invalid.inspect}" unless invalid.empty? + end + end + + # :section: should_be_restful + # Generates a full suite of tests for a restful controller. + # + # The following definition will generate tests for the +index+, +show+, +new+, + # +edit+, +create+, +update+ and +destroy+ actions, in both +html+ and +xml+ formats: + # + # should_be_restful do |resource| + # resource.parent = :user + # + # resource.create.params = { :title => "first post", :body => 'blah blah blah'} + # resource.update.params = { :title => "changed" } + # end + # + # This generates about 40 tests, all of the format: + # "on GET to :show should assign @user." + # "on GET to :show should not set the flash." + # "on GET to :show should render 'show' template." + # "on GET to :show should respond with success." + # "on GET to :show as xml should assign @user." + # "on GET to :show as xml should have ContentType set to 'application/xml'." + # "on GET to :show as xml should respond with success." + # "on GET to :show as xml should return as the root element." + # The +resource+ parameter passed into the block is a ResourceOptions object, and + # is used to configure the tests for the details of your resources. + # + def should_be_restful(&blk) # :yields: resource + resource = ResourceOptions.new + blk.call(resource) + resource.normalize!(self) + + resource.formats.each do |format| + resource.actions.each do |action| + if self.respond_to? :"make_#{action}_#{format}_tests" + self.send(:"make_#{action}_#{format}_tests", resource) + else + should "test #{action} #{format}" do + flunk "Test for #{action} as #{format} not implemented" end end end end + end - # :section: Test macros - - # Macro that creates a test asserting that the flash contains the given value. - # val can be a String, a Regex, or nil (indicating that the flash should not be set) - # - # Example: - # - # should_set_the_flash_to "Thank you for placing this order." - # should_set_the_flash_to /created/i - # should_set_the_flash_to nil - def should_set_the_flash_to(val) - if val - should "have #{val.inspect} in the flash" do - assert_contains flash.values, val, ", Flash: #{flash.inspect}" - end - else - should "not set the flash" do - assert_equal({}, flash, "Flash was set to:\n#{flash.inspect}") - end + # :section: Test macros + + # Macro that creates a test asserting that the flash contains the given value. + # val can be a String, a Regex, or nil (indicating that the flash should not be set) + # + # Example: + # + # should_set_the_flash_to "Thank you for placing this order." + # should_set_the_flash_to /created/i + # should_set_the_flash_to nil + def should_set_the_flash_to(val) + if val + should "have #{val.inspect} in the flash" do + assert_contains flash.values, val, ", Flash: #{flash.inspect}" end - end - - # Macro that creates a test asserting that the flash is empty. Same as - # @should_set_the_flash_to nil@ - def should_not_set_the_flash - should_set_the_flash_to nil - end - - # Macro that creates a test asserting that the controller assigned to @name - # - # Example: - # - # should_assign_to :user - def should_assign_to(name) - should "assign @#{name}" do - assert assigns(name.to_sym), "The action isn't assigning to @#{name}" - end - end - - # Macro that creates a test asserting that the controller did not assign to @name - # - # Example: - # - # should_not_assign_to :user - def should_not_assign_to(name) - should "not assign to @#{name}" do - assert !assigns(name.to_sym), "@#{name} was visible" - end - end - - # Macro that creates a test asserting that the controller responded with a 'response' status code. - # Example: - # - # should_respond_with :success - def should_respond_with(response) - should "respond with #{response}" do - assert_response response - end - end - - # Macro that creates a test asserting that the controller rendered the given template. - # Example: - # - # should_render_template :new - def should_render_template(template) - should "render template #{template.inspect}" do - assert_template template.to_s - end - end - - # Macro that creates a test asserting that the controller returned a redirect to the given path. - # The given string is evaled to produce the resulting redirect path. All of the instance variables - # set by the controller are available to the evaled string. - # Example: - # - # should_redirect_to '"/"' - # should_redirect_to "users_url(@user)" - def should_redirect_to(url) - should "redirect to #{url.inspect}" do - instantiate_variables_from_assigns do - assert_redirected_to eval(url, self.send(:binding), __FILE__, __LINE__) - end - end - end - - # Macro that creates a test asserting that the rendered view contains a
element. - def should_render_a_form - should "display a form" do - assert_select "form", true, "The template doesn't contain a element" + else + should "not set the flash" do + assert_equal({}, flash, "Flash was set to:\n#{flash.inspect}") end end end - - module InstanceMethods # :nodoc: - - private # :enddoc: - - SPECIAL_INSTANCE_VARIABLES = %w{ - _cookies - _flash - _headers - _params - _request - _response - _session - action_name - before_filter_chain_aborted - cookies - flash - headers - ignore_missing_templates - logger - params - request - request_origin - response - session - template - template_class - template_root - url - variables_added - }.map(&:to_s) - - def instantiate_variables_from_assigns(*names, &blk) - old = {} - names = (@response.template.assigns.keys - SPECIAL_INSTANCE_VARIABLES) if names.empty? - names.each do |name| - old[name] = instance_variable_get("@#{name}") - instance_variable_set("@#{name}", assigns(name.to_sym)) - end - blk.call - names.each do |name| - instance_variable_set("@#{name}", old[name]) - end - end - - def get_existing_record(res) # :nodoc: - returning(instance_variable_get("@#{res.object}")) do |record| - assert(record, "This test requires you to set @#{res.object} in your setup block") - end - end - - def make_parent_params(resource, record = nil, parent_names = nil) # :nodoc: - parent_names ||= resource.parents.reverse - return {} if parent_names == [] # Base case - parent_name = parent_names.shift - parent = record ? record.send(parent_name) : parent_name.to_s.classify.constantize.find(:first) - - { :"#{parent_name}_id" => parent.to_param }.merge(make_parent_params(resource, parent, parent_names)) - end - + + # Macro that creates a test asserting that the flash is empty. Same as + # @should_set_the_flash_to nil@ + def should_not_set_the_flash + should_set_the_flash_to nil end - end - end + + # Macro that creates a test asserting that the controller assigned to @name + # + # Example: + # + # should_assign_to :user + def should_assign_to(name) + should "assign @#{name}" do + assert assigns(name.to_sym), "The action isn't assigning to @#{name}" + end + end + + # Macro that creates a test asserting that the controller did not assign to @name + # + # Example: + # + # should_not_assign_to :user + def should_not_assign_to(name) + should "not assign to @#{name}" do + assert !assigns(name.to_sym), "@#{name} was visible" + end + end + + # Macro that creates a test asserting that the controller responded with a 'response' status code. + # Example: + # + # should_respond_with :success + def should_respond_with(response) + should "respond with #{response}" do + assert_response response + end + end + + # Macro that creates a test asserting that the controller rendered the given template. + # Example: + # + # should_render_template :new + def should_render_template(template) + should "render template #{template.inspect}" do + assert_template template.to_s + end + end + + # Macro that creates a test asserting that the controller returned a redirect to the given path. + # The given string is evaled to produce the resulting redirect path. All of the instance variables + # set by the controller are available to the evaled string. + # Example: + # + # should_redirect_to '"/"' + # should_redirect_to "users_url(@user)" + def should_redirect_to(url) + should "redirect to #{url.inspect}" do + instantiate_variables_from_assigns do + assert_redirected_to eval(url, self.send(:binding), __FILE__, __LINE__) + end + end + end + + # Macro that creates a test asserting that the rendered view contains a element. + def should_render_a_form + should "display a form" do + assert_select "form", true, "The template doesn't contain a element" + end + end + end + + module InstanceMethods # :nodoc: + + private # :enddoc: + + SPECIAL_INSTANCE_VARIABLES = %w{ + _cookies + _flash + _headers + _params + _request + _response + _session + action_name + before_filter_chain_aborted + cookies + flash + headers + ignore_missing_templates + logger + params + request + request_origin + response + session + template + template_class + template_root + url + variables_added + }.map(&:to_s) + + def instantiate_variables_from_assigns(*names, &blk) + old = {} + names = (@response.template.assigns.keys - SPECIAL_INSTANCE_VARIABLES) if names.empty? + names.each do |name| + old[name] = instance_variable_get("@#{name}") + instance_variable_set("@#{name}", assigns(name.to_sym)) + end + blk.call + names.each do |name| + instance_variable_set("@#{name}", old[name]) + end + end + + def get_existing_record(res) # :nodoc: + returning(instance_variable_get("@#{res.object}")) do |record| + assert(record, "This test requires you to set @#{res.object} in your setup block") + end + end + + def make_parent_params(resource, record = nil, parent_names = nil) # :nodoc: + parent_names ||= resource.parents.reverse + return {} if parent_names == [] # Base case + parent_name = parent_names.shift + parent = record ? record.send(parent_name) : parent_name.to_s.classify.constantize.find(:first) + + { :"#{parent_name}_id" => parent.to_param }.merge(make_parent_params(resource, parent, parent_names)) + end + + end + end end - diff --git a/lib/shoulda/controller_tests/formats/html.rb b/lib/shoulda/controller_tests/formats/html.rb index ba92bba8..1f77398f 100644 --- a/lib/shoulda/controller_tests/formats/html.rb +++ b/lib/shoulda/controller_tests/formats/html.rb @@ -1,196 +1,194 @@ -module ThoughtBot # :nodoc: - module Shoulda # :nodoc: - module Controller # :nodoc: - module HTML # :nodoc: all - def self.included(other) - other.class_eval do - extend ThoughtBot::Shoulda::Controller::HTML::ClassMethods +module Shoulda # :nodoc: + module Controller # :nodoc: + module HTML # :nodoc: all + def self.included(other) + other.class_eval do + extend Shoulda::Controller::HTML::ClassMethods + end + end + + module ClassMethods + def controller_name_from_class + self.name.gsub(/Test/, '') + end + + def make_show_html_tests(res) + context "on GET to #{controller_name_from_class}#show" do + setup do + record = get_existing_record(res) + parent_params = make_parent_params(res, record) + get :show, parent_params.merge({ res.identifier => record.to_param }) + end + + if res.denied.actions.include?(:show) + should_not_assign_to res.object + should_redirect_to res.denied.redirect + should_set_the_flash_to res.denied.flash + else + should_assign_to res.object + should_respond_with :success + should_render_template :show + should_not_set_the_flash + end end end - - module ClassMethods - def controller_name_from_class - self.name.gsub(/Test/, '') - end - def make_show_html_tests(res) - context "on GET to #{controller_name_from_class}#show" do - setup do - record = get_existing_record(res) - parent_params = make_parent_params(res, record) - get :show, parent_params.merge({ res.identifier => record.to_param }) - end - - if res.denied.actions.include?(:show) - should_not_assign_to res.object - should_redirect_to res.denied.redirect - should_set_the_flash_to res.denied.flash - else - should_assign_to res.object - should_respond_with :success - should_render_template :show - should_not_set_the_flash + def make_edit_html_tests(res) + context "on GET to #{controller_name_from_class}#edit" do + setup do + @record = get_existing_record(res) + parent_params = make_parent_params(res, @record) + get :edit, parent_params.merge({ res.identifier => @record.to_param }) + end + + if res.denied.actions.include?(:edit) + should_not_assign_to res.object + should_redirect_to res.denied.redirect + should_set_the_flash_to res.denied.flash + else + should_assign_to res.object + should_respond_with :success + should_render_template :edit + should_not_set_the_flash + should_render_a_form + should "set @#{res.object} to requested instance" do + assert_equal @record, assigns(res.object) end end end + end - def make_edit_html_tests(res) - context "on GET to #{controller_name_from_class}#edit" do - setup do - @record = get_existing_record(res) - parent_params = make_parent_params(res, @record) - get :edit, parent_params.merge({ res.identifier => @record.to_param }) - end + def make_index_html_tests(res) + context "on GET to #{controller_name_from_class}#index" do + setup do + record = get_existing_record(res) rescue nil + parent_params = make_parent_params(res, record) + get(:index, parent_params) + end + + if res.denied.actions.include?(:index) + should_not_assign_to res.object.to_s.pluralize + should_redirect_to res.denied.redirect + should_set_the_flash_to res.denied.flash + else + should_respond_with :success + should_assign_to res.object.to_s.pluralize + should_render_template :index + should_not_set_the_flash + end + end + end + + def make_new_html_tests(res) + context "on GET to #{controller_name_from_class}#new" do + setup do + record = get_existing_record(res) rescue nil + parent_params = make_parent_params(res, record) + get(:new, parent_params) + end + + if res.denied.actions.include?(:new) + should_not_assign_to res.object + should_redirect_to res.denied.redirect + should_set_the_flash_to res.denied.flash + else + should_respond_with :success + should_assign_to res.object + should_not_set_the_flash + should_render_template :new + should_render_a_form + end + end + end + + def make_destroy_html_tests(res) + context "on DELETE to #{controller_name_from_class}#destroy" do + setup do + @record = get_existing_record(res) + parent_params = make_parent_params(res, @record) + delete :destroy, parent_params.merge({ res.identifier => @record.to_param }) + end + + if res.denied.actions.include?(:destroy) + should_redirect_to res.denied.redirect + should_set_the_flash_to res.denied.flash - if res.denied.actions.include?(:edit) - should_not_assign_to res.object - should_redirect_to res.denied.redirect - should_set_the_flash_to res.denied.flash + should "not destroy record" do + assert_nothing_raised { assert @record.reload } + end + else + should_set_the_flash_to res.destroy.flash + if res.destroy.redirect.is_a? Symbol + should_respond_with res.destroy.redirect else - should_assign_to res.object - should_respond_with :success - should_render_template :edit - should_not_set_the_flash - should_render_a_form - should "set @#{res.object} to requested instance" do - assert_equal @record, assigns(res.object) + should_redirect_to res.destroy.redirect + end + + should "destroy record" do + assert_raises(::ActiveRecord::RecordNotFound, "@#{res.object} was not destroyed.") do + @record.reload end end end end + end - def make_index_html_tests(res) - context "on GET to #{controller_name_from_class}#index" do - setup do - record = get_existing_record(res) rescue nil - parent_params = make_parent_params(res, record) - get(:index, parent_params) - end - - if res.denied.actions.include?(:index) - should_not_assign_to res.object.to_s.pluralize - should_redirect_to res.denied.redirect - should_set_the_flash_to res.denied.flash - else - should_respond_with :success - should_assign_to res.object.to_s.pluralize - should_render_template :index - should_not_set_the_flash - end + def make_create_html_tests(res) + context "on POST to #{controller_name_from_class}#create with #{res.create.params.inspect}" do + setup do + record = get_existing_record(res) rescue nil + parent_params = make_parent_params(res, record) + @count = res.klass.count + post :create, parent_params.merge(res.object => res.create.params) end - end - - def make_new_html_tests(res) - context "on GET to #{controller_name_from_class}#new" do - setup do - record = get_existing_record(res) rescue nil - parent_params = make_parent_params(res, record) - get(:new, parent_params) - end - - if res.denied.actions.include?(:new) - should_not_assign_to res.object - should_redirect_to res.denied.redirect - should_set_the_flash_to res.denied.flash - else - should_respond_with :success - should_assign_to res.object - should_not_set_the_flash - should_render_template :new - should_render_a_form - end - end - end - - def make_destroy_html_tests(res) - context "on DELETE to #{controller_name_from_class}#destroy" do - setup do - @record = get_existing_record(res) - parent_params = make_parent_params(res, @record) - delete :destroy, parent_params.merge({ res.identifier => @record.to_param }) - end + + if res.denied.actions.include?(:create) + should_redirect_to res.denied.redirect + should_set_the_flash_to res.denied.flash + should_not_assign_to res.object - if res.denied.actions.include?(:destroy) - should_redirect_to res.denied.redirect - should_set_the_flash_to res.denied.flash - - should "not destroy record" do - assert_nothing_raised { assert @record.reload } - end + should "not create new record" do + assert_equal @count, res.klass.count + end + else + should_assign_to res.object + should_set_the_flash_to res.create.flash + if res.create.redirect.is_a? Symbol + should_respond_with res.create.redirect else - should_set_the_flash_to res.destroy.flash - if res.destroy.redirect.is_a? Symbol - should_respond_with res.destroy.redirect - else - should_redirect_to res.destroy.redirect - end - - should "destroy record" do - assert_raises(::ActiveRecord::RecordNotFound, "@#{res.object} was not destroyed.") do - @record.reload - end - end + should_redirect_to res.create.redirect end - end + + should "not have errors on @#{res.object}" do + assert_equal [], pretty_error_messages(assigns(res.object)), "@#{res.object} has errors:" + end + end end + end - def make_create_html_tests(res) - context "on POST to #{controller_name_from_class}#create with #{res.create.params.inspect}" do - setup do - record = get_existing_record(res) rescue nil - parent_params = make_parent_params(res, record) - @count = res.klass.count - post :create, parent_params.merge(res.object => res.create.params) - end - - if res.denied.actions.include?(:create) - should_redirect_to res.denied.redirect - should_set_the_flash_to res.denied.flash - should_not_assign_to res.object - - should "not create new record" do - assert_equal @count, res.klass.count - end - else - should_assign_to res.object - should_set_the_flash_to res.create.flash - if res.create.redirect.is_a? Symbol - should_respond_with res.create.redirect - else - should_redirect_to res.create.redirect - end - - should "not have errors on @#{res.object}" do - assert_equal [], pretty_error_messages(assigns(res.object)), "@#{res.object} has errors:" - end - end + def make_update_html_tests(res) + context "on PUT to #{controller_name_from_class}#update with #{res.create.params.inspect}" do + setup do + @record = get_existing_record(res) + parent_params = make_parent_params(res, @record) + put :update, parent_params.merge(res.identifier => @record.to_param, res.object => res.update.params) end - end - def make_update_html_tests(res) - context "on PUT to #{controller_name_from_class}#update with #{res.create.params.inspect}" do - setup do - @record = get_existing_record(res) - parent_params = make_parent_params(res, @record) - put :update, parent_params.merge(res.identifier => @record.to_param, res.object => res.update.params) - end - - if res.denied.actions.include?(:update) - should_not_assign_to res.object - should_redirect_to res.denied.redirect - should_set_the_flash_to res.denied.flash + if res.denied.actions.include?(:update) + should_not_assign_to res.object + should_redirect_to res.denied.redirect + should_set_the_flash_to res.denied.flash + else + should_assign_to res.object + should_set_the_flash_to(res.update.flash) + if res.update.redirect.is_a? Symbol + should_respond_with res.update.redirect else - should_assign_to res.object - should_set_the_flash_to(res.update.flash) - if res.update.redirect.is_a? Symbol - should_respond_with res.update.redirect - else - should_redirect_to res.update.redirect - end - - should "not have errors on @#{res.object}" do - assert_equal [], pretty_error_messages(assigns(res.object)), "@#{res.object} has errors:" - end + should_redirect_to res.update.redirect + end + + should "not have errors on @#{res.object}" do + assert_equal [], pretty_error_messages(assigns(res.object)), "@#{res.object} has errors:" end end end diff --git a/lib/shoulda/controller_tests/formats/xml.rb b/lib/shoulda/controller_tests/formats/xml.rb index f3c16f18..c8c97fdb 100644 --- a/lib/shoulda/controller_tests/formats/xml.rb +++ b/lib/shoulda/controller_tests/formats/xml.rb @@ -1,170 +1,168 @@ -module ThoughtBot # :nodoc: - module Shoulda # :nodoc: - module Controller # :nodoc: - module XML - def self.included(other) #:nodoc: - other.class_eval do - extend ThoughtBot::Shoulda::Controller::XML::ClassMethods - end +module Shoulda # :nodoc: + module Controller # :nodoc: + module XML + def self.included(other) #:nodoc: + other.class_eval do + extend Shoulda::Controller::XML::ClassMethods end - - module ClassMethods - # Macro that creates a test asserting that the controller responded with an XML content-type - # and that the XML contains ++ as the root element. - def should_respond_with_xml_for(name = nil) - should "have ContentType set to 'application/xml'" do - assert_xml_response - end - - if name - should "return <#{name}/> as the root element" do - body = @response.body.first(100).map {|l| " #{l}"} - assert_select name.to_s.dasherize, 1, "Body:\n#{body}...\nDoes not have <#{name}/> as the root element." - end - end - end - alias should_respond_with_xml should_respond_with_xml_for - - protected - - def make_show_xml_tests(res) # :nodoc: - context "on GET to #{controller_name_from_class}#show as xml" do - setup do - request_xml - record = get_existing_record(res) - parent_params = make_parent_params(res, record) - get :show, parent_params.merge({ res.identifier => record.to_param }) - end - - if res.denied.actions.include?(:show) - should_not_assign_to res.object - should_respond_with 401 - else - should_assign_to res.object - should_respond_with :success - should_respond_with_xml_for res.object - end - end - end - - def make_edit_xml_tests(res) # :nodoc: - # XML doesn't need an :edit action - end - - def make_new_xml_tests(res) # :nodoc: - # XML doesn't need a :new action - end - - def make_index_xml_tests(res) # :nodoc: - context "on GET to #{controller_name_from_class}#index as xml" do - setup do - request_xml - parent_params = make_parent_params(res) - get(:index, parent_params) - end - - if res.denied.actions.include?(:index) - should_not_assign_to res.object.to_s.pluralize - should_respond_with 401 - else - should_respond_with :success - should_respond_with_xml_for res.object.to_s.pluralize - should_assign_to res.object.to_s.pluralize - end - end - end - - def make_destroy_xml_tests(res) # :nodoc: - context "on DELETE to #{controller_name_from_class}#destroy as xml" do - setup do - request_xml - @record = get_existing_record(res) - parent_params = make_parent_params(res, @record) - delete :destroy, parent_params.merge({ res.identifier => @record.to_param }) - end - - if res.denied.actions.include?(:destroy) - should_respond_with 401 - - should "not destroy record" do - assert @record.reload - end - else - should "destroy record" do - assert_raises(::ActiveRecord::RecordNotFound, "@#{res.object} was not destroyed.") do - @record.reload - end - end - end - end - end - - def make_create_xml_tests(res) # :nodoc: - context "on POST to #{controller_name_from_class}#create as xml" do - setup do - request_xml - parent_params = make_parent_params(res) - @count = res.klass.count - post :create, parent_params.merge(res.object => res.create.params) - end - - if res.denied.actions.include?(:create) - should_respond_with 401 - should_not_assign_to res.object - - should "not create new record" do - assert_equal @count, res.klass.count - end - else - should_assign_to res.object - - should "not have errors on @#{res.object}" do - assert_equal [], pretty_error_messages(assigns(res.object)), "@#{res.object} has errors:" - end - end - end - end - - def make_update_xml_tests(res) # :nodoc: - context "on PUT to #{controller_name_from_class}#update as xml" do - setup do - request_xml - @record = get_existing_record(res) - parent_params = make_parent_params(res, @record) - put :update, parent_params.merge(res.identifier => @record.to_param, res.object => res.update.params) - end - - if res.denied.actions.include?(:update) - should_not_assign_to res.object - should_respond_with 401 - else - should_assign_to res.object - - should "not have errors on @#{res.object}" do - assert_equal [], assigns(res.object).errors.full_messages, "@#{res.object} has errors:" - end - end - end - end - end - - # Sets the next request's format to 'application/xml' - def request_xml - @request.accept = "application/xml" - end - - # Asserts that the controller's response was 'application/xml' - def assert_xml_response - content_type = (@response.headers["Content-Type"] || @response.headers["type"]).to_s - regex = %r{\bapplication/xml\b} - - msg = "Content Type '#{content_type.inspect}' doesn't match '#{regex.inspect}'\n" - msg += "Body: #{@response.body.first(100).chomp} ..." - - assert_match regex, content_type, msg - end - end + + module ClassMethods + # Macro that creates a test asserting that the controller responded with an XML content-type + # and that the XML contains ++ as the root element. + def should_respond_with_xml_for(name = nil) + should "have ContentType set to 'application/xml'" do + assert_xml_response + end + + if name + should "return <#{name}/> as the root element" do + body = @response.body.first(100).map {|l| " #{l}"} + assert_select name.to_s.dasherize, 1, "Body:\n#{body}...\nDoes not have <#{name}/> as the root element." + end + end + end + alias should_respond_with_xml should_respond_with_xml_for + + protected + + def make_show_xml_tests(res) # :nodoc: + context "on GET to #{controller_name_from_class}#show as xml" do + setup do + request_xml + record = get_existing_record(res) + parent_params = make_parent_params(res, record) + get :show, parent_params.merge({ res.identifier => record.to_param }) + end + + if res.denied.actions.include?(:show) + should_not_assign_to res.object + should_respond_with 401 + else + should_assign_to res.object + should_respond_with :success + should_respond_with_xml_for res.object + end + end + end + + def make_edit_xml_tests(res) # :nodoc: + # XML doesn't need an :edit action + end + + def make_new_xml_tests(res) # :nodoc: + # XML doesn't need a :new action + end + + def make_index_xml_tests(res) # :nodoc: + context "on GET to #{controller_name_from_class}#index as xml" do + setup do + request_xml + parent_params = make_parent_params(res) + get(:index, parent_params) + end + + if res.denied.actions.include?(:index) + should_not_assign_to res.object.to_s.pluralize + should_respond_with 401 + else + should_respond_with :success + should_respond_with_xml_for res.object.to_s.pluralize + should_assign_to res.object.to_s.pluralize + end + end + end + + def make_destroy_xml_tests(res) # :nodoc: + context "on DELETE to #{controller_name_from_class}#destroy as xml" do + setup do + request_xml + @record = get_existing_record(res) + parent_params = make_parent_params(res, @record) + delete :destroy, parent_params.merge({ res.identifier => @record.to_param }) + end + + if res.denied.actions.include?(:destroy) + should_respond_with 401 + + should "not destroy record" do + assert @record.reload + end + else + should "destroy record" do + assert_raises(::ActiveRecord::RecordNotFound, "@#{res.object} was not destroyed.") do + @record.reload + end + end + end + end + end + + def make_create_xml_tests(res) # :nodoc: + context "on POST to #{controller_name_from_class}#create as xml" do + setup do + request_xml + parent_params = make_parent_params(res) + @count = res.klass.count + post :create, parent_params.merge(res.object => res.create.params) + end + + if res.denied.actions.include?(:create) + should_respond_with 401 + should_not_assign_to res.object + + should "not create new record" do + assert_equal @count, res.klass.count + end + else + should_assign_to res.object + + should "not have errors on @#{res.object}" do + assert_equal [], pretty_error_messages(assigns(res.object)), "@#{res.object} has errors:" + end + end + end + end + + def make_update_xml_tests(res) # :nodoc: + context "on PUT to #{controller_name_from_class}#update as xml" do + setup do + request_xml + @record = get_existing_record(res) + parent_params = make_parent_params(res, @record) + put :update, parent_params.merge(res.identifier => @record.to_param, res.object => res.update.params) + end + + if res.denied.actions.include?(:update) + should_not_assign_to res.object + should_respond_with 401 + else + should_assign_to res.object + + should "not have errors on @#{res.object}" do + assert_equal [], assigns(res.object).errors.full_messages, "@#{res.object} has errors:" + end + end + end + end + end + + # Sets the next request's format to 'application/xml' + def request_xml + @request.accept = "application/xml" + end + + # Asserts that the controller's response was 'application/xml' + def assert_xml_response + content_type = (@response.headers["Content-Type"] || @response.headers["type"]).to_s + regex = %r{\bapplication/xml\b} + + msg = "Content Type '#{content_type.inspect}' doesn't match '#{regex.inspect}'\n" + msg += "Body: #{@response.body.first(100).chomp} ..." + + assert_match regex, content_type, msg + end + end end end diff --git a/lib/shoulda/gem/shoulda.rb b/lib/shoulda/gem/shoulda.rb index d2293716..19163089 100644 --- a/lib/shoulda/gem/shoulda.rb +++ b/lib/shoulda/gem/shoulda.rb @@ -1,240 +1,238 @@ require File.join(File.dirname(__FILE__), 'proc_extensions') -module Thoughtbot - module Shoulda - class << self - attr_accessor :current_context - end +module Shoulda + class << self + attr_accessor :current_context + end - VERSION = '1.1.1' + VERSION = '1.1.1' - # = Should statements - # - # Should statements are just syntactic sugar over normal Test::Unit test methods. A should block - # contains all the normal code and assertions you're used to seeing, with the added benefit that - # they can be wrapped inside context blocks (see below). - # - # == Example: - # - # class UserTest << Test::Unit::TestCase - # - # def setup - # @user = User.new("John", "Doe") - # end - # - # should "return its full name" - # assert_equal 'John Doe', @user.full_name - # end - # - # end - # - # ...will produce the following test: - # * "test: User should return its full name. " - # - # Note: The part before should in the test name is gleamed from the name of the Test::Unit class. + # = Should statements + # + # Should statements are just syntactic sugar over normal Test::Unit test methods. A should block + # contains all the normal code and assertions you're used to seeing, with the added benefit that + # they can be wrapped inside context blocks (see below). + # + # == Example: + # + # class UserTest << Test::Unit::TestCase + # + # def setup + # @user = User.new("John", "Doe") + # end + # + # should "return its full name" + # assert_equal 'John Doe', @user.full_name + # end + # + # end + # + # ...will produce the following test: + # * "test: User should return its full name. " + # + # Note: The part before should in the test name is gleamed from the name of the Test::Unit class. - def should(name, &blk) - should_eventually(name) && return unless block_given? - - if Shoulda.current_context - Shoulda.current_context.should(name, &blk) - else - context_name = self.name.gsub(/Test/, "") - context = Thoughtbot::Shoulda::Context.new(context_name, self) do - should(name, &blk) - end - context.build - end - end - - # Just like should, but never runs, and instead prints an 'X' in the Test::Unit output. - def should_eventually(name, &blk) + def should(name, &blk) + should_eventually(name) && return unless block_given? + + if Shoulda.current_context + Shoulda.current_context.should(name, &blk) + else context_name = self.name.gsub(/Test/, "") - context = Thoughtbot::Shoulda::Context.new(context_name, self) do - should_eventually(name, &blk) + context = Shoulda::Context.new(context_name, self) do + should(name, &blk) end context.build end + end - # = Contexts - # - # A context block groups should statements under a common set of setup/teardown methods. - # Context blocks can be arbitrarily nested, and can do wonders for improving the maintainability - # and readability of your test code. - # - # A context block can contain setup, should, should_eventually, and teardown blocks. - # - # class UserTest << Test::Unit::TestCase - # 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 "test: A User instance should return its full name. ". - # - # Contexts may be nested. Nested contexts run their setup blocks from out to in before each - # should statement. They then run their teardown blocks from in to out after each should statement. - # - # class UserTest << Test::Unit::TestCase - # 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 - # * "test: A User instance should return its full name. " - # * "test: A User instance with a profile should return true when sent :has_profile?. " - # - # Just like should statements, a context block can exist next to normal def test_the_old_way; end - # tests. This means you do not have to fully commit to the context/should syntax in a test file. + # Just like should, but never runs, and instead prints an 'X' in the Test::Unit output. + def should_eventually(name, &blk) + context_name = self.name.gsub(/Test/, "") + context = Shoulda::Context.new(context_name, self) do + should_eventually(name, &blk) + end + context.build + end + + # = Contexts + # + # A context block groups should statements under a common set of setup/teardown methods. + # Context blocks can be arbitrarily nested, and can do wonders for improving the maintainability + # and readability of your test code. + # + # A context block can contain setup, should, should_eventually, and teardown blocks. + # + # class UserTest << Test::Unit::TestCase + # 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 "test: A User instance should return its full name. ". + # + # Contexts may be nested. Nested contexts run their setup blocks from out to in before each + # should statement. They then run their teardown blocks from in to out after each should statement. + # + # class UserTest << Test::Unit::TestCase + # 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 + # * "test: A User instance should return its full name. " + # * "test: A User instance with a profile should return true when sent :has_profile?. " + # + # Just like should statements, a context block can exist next to normal def test_the_old_way; end + # tests. This means you do not have to fully commit to the context/should syntax in a test file. + + def context(name, &blk) + if Shoulda.current_context + Shoulda.current_context.context(name, &blk) + else + context = Shoulda::Context.new(name, self, &blk) + context.build + end + end + + class Context # :nodoc: + + attr_accessor :name # my name + attr_accessor :parent # may be another context, or the original test::unit class. + attr_accessor :subcontexts # array of contexts nested under myself + attr_accessor :setup_block # block given via a setup method + attr_accessor :teardown_block # block given via a teardown method + attr_accessor :shoulds # array of hashes representing the should statements + attr_accessor :should_eventuallys # array of hashes representing the should eventually statements + + def initialize(name, parent, &blk) + Shoulda.current_context = self + self.name = name + self.parent = parent + self.setup_block = nil + self.teardown_block = nil + self.shoulds = [] + self.should_eventuallys = [] + self.subcontexts = [] + + blk.bind(self).call + Shoulda.current_context = nil + end def context(name, &blk) - if Shoulda.current_context - Shoulda.current_context.context(name, &blk) - else - context = Thoughtbot::Shoulda::Context.new(name, self, &blk) - context.build + subcontexts << Context.new(name, self, &blk) + Shoulda.current_context = self + end + + def setup(&blk) + self.setup_block = blk + end + + def teardown(&blk) + self.teardown_block = blk + end + + def should(name, &blk) + self.shoulds << { :name => name, :block => blk } + end + + def should_eventually(name, &blk) + self.should_eventuallys << { :name => name, :block => blk } + end + + def full_name + parent_name = parent.full_name if am_subcontext? + return [parent_name, name].join(" ").strip + end + + def am_subcontext? + parent.is_a?(self.class) # my parent is the same class as myself. + end + + def test_unit_class + am_subcontext? ? parent.test_unit_class : parent + end + + def create_test_from_should_hash(should) + test_name = ["test:", full_name, "should", "#{should[:name]}. "].flatten.join(' ').to_sym + + if test_unit_class.instance_methods.include?(test_name.to_s) + warn " * WARNING: '#{test_name}' is already defined" + end + + context = self + test_unit_class.send(:define_method, test_name) do |*args| + begin + context.run_all_setup_blocks(self) + should[:block].bind(self).call + ensure + context.run_all_teardown_blocks(self) + end end end - class Context # :nodoc: - - attr_accessor :name # my name - attr_accessor :parent # may be another context, or the original test::unit class. - attr_accessor :subcontexts # array of contexts nested under myself - attr_accessor :setup_block # block given via a setup method - attr_accessor :teardown_block # block given via a teardown method - attr_accessor :shoulds # array of hashes representing the should statements - attr_accessor :should_eventuallys # array of hashes representing the should eventually statements - - def initialize(name, parent, &blk) - Shoulda.current_context = self - self.name = name - self.parent = parent - self.setup_block = nil - self.teardown_block = nil - self.shoulds = [] - self.should_eventuallys = [] - self.subcontexts = [] - - blk.bind(self).call - Shoulda.current_context = nil - end - - def context(name, &blk) - subcontexts << Context.new(name, self, &blk) - Shoulda.current_context = self - end - - def setup(&blk) - self.setup_block = blk - end - - def teardown(&blk) - self.teardown_block = blk - end - - def should(name, &blk) - self.shoulds << { :name => name, :block => blk } - end - - def should_eventually(name, &blk) - self.should_eventuallys << { :name => name, :block => blk } - end - - def full_name - parent_name = parent.full_name if am_subcontext? - return [parent_name, name].join(" ").strip - end - - def am_subcontext? - parent.is_a?(self.class) # my parent is the same class as myself. - end - - def test_unit_class - am_subcontext? ? parent.test_unit_class : parent - end - - def create_test_from_should_hash(should) - test_name = ["test:", full_name, "should", "#{should[:name]}. "].flatten.join(' ').to_sym - - if test_unit_class.instance_methods.include?(test_name.to_s) - warn " * WARNING: '#{test_name}' is already defined" - end - - context = self - test_unit_class.send(:define_method, test_name) do |*args| - begin - context.run_all_setup_blocks(self) - should[:block].bind(self).call - ensure - context.run_all_teardown_blocks(self) - end - end - end - - def run_all_setup_blocks(binding) - self.parent.run_all_setup_blocks(binding) if am_subcontext? - setup_block.bind(binding).call if setup_block - end - - def run_all_teardown_blocks(binding) - teardown_block.bind(binding).call if teardown_block - self.parent.run_all_teardown_blocks(binding) if am_subcontext? - end - - def print_should_eventuallys - should_eventuallys.each do |should| - test_name = [full_name, "should", "#{should[:name]}. "].flatten.join(' ') - puts " * DEFERRED: " + test_name - end - subcontexts.each { |context| context.print_should_eventuallys } - end - - def build - shoulds.each do |should| - create_test_from_should_hash(should) - end - - subcontexts.each { |context| context.build } - - print_should_eventuallys - end - - def method_missing(method, *args, &blk) - test_unit_class.send(method, *args, &blk) - end - + def run_all_setup_blocks(binding) + self.parent.run_all_setup_blocks(binding) if am_subcontext? + setup_block.bind(binding).call if setup_block end + + def run_all_teardown_blocks(binding) + teardown_block.bind(binding).call if teardown_block + self.parent.run_all_teardown_blocks(binding) if am_subcontext? + end + + def print_should_eventuallys + should_eventuallys.each do |should| + test_name = [full_name, "should", "#{should[:name]}. "].flatten.join(' ') + puts " * DEFERRED: " + test_name + end + subcontexts.each { |context| context.print_should_eventuallys } + end + + def build + shoulds.each do |should| + create_test_from_should_hash(should) + end + + subcontexts.each { |context| context.build } + + print_should_eventuallys + end + + def method_missing(method, *args, &blk) + test_unit_class.send(method, *args, &blk) + end + end end module Test # :nodoc: all module Unit class TestCase - extend Thoughtbot::Shoulda + extend Shoulda end end end diff --git a/lib/shoulda/general.rb b/lib/shoulda/general.rb index 7b1b36b6..16f0e2cb 100644 --- a/lib/shoulda/general.rb +++ b/lib/shoulda/general.rb @@ -1,118 +1,116 @@ -module ThoughtBot # :nodoc: - module Shoulda # :nodoc: - module General - def self.included(other) # :nodoc: - other.class_eval do - extend ThoughtBot::Shoulda::General::ClassMethods - end +module Shoulda # :nodoc: + module General + def self.included(other) # :nodoc: + other.class_eval do + extend Shoulda::General::ClassMethods end - - module ClassMethods - # Loads all fixture files (test/fixtures/*.yml) - def load_all_fixtures - all_fixtures = Dir.glob(File.join(Test::Unit::TestCase.fixture_path, "*.yml")).collect do |f| - File.basename(f, '.yml').to_sym - end - fixtures *all_fixtures - end - end - - # Prints a message to stdout, tagged with the name of the calling method. - def report!(msg = "") - puts("#{caller.first}: #{msg}") - 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 - # assert_contains(['a', '1'], 'a') => passes - # assert_contains(['a', '1'], /not there/) => fails - def assert_contains(collection, x, extra_msg = "") - collection = [collection] unless collection.is_a?(Array) - msg = "#{x.inspect} 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.inspect} 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 object can be saved - # - # assert_save User.new(params) - def assert_save(obj) - assert obj.save, "Errors: #{pretty_error_messages obj}" - obj.reload - end - - # Asserts that the given object is valid - # - # assert_valid User.new(params) - def assert_valid(obj) - assert obj.valid?, "Errors: #{pretty_error_messages obj}" - end - - # Asserts that an email was delivered. Can take a block that can further - # narrow down the types of emails you're expecting. - # - # assert_sent_email - # - # Passes if ActionMailer::Base.deliveries has an email - # - # assert_sent_email do |email| - # email.subject =~ /hi there/ && email.to.include?('none@none.com') - # end - # - # Passes if there is an email with subject containing 'hi there' and - # 'none@none.com' as one of the recipients. - # - def assert_sent_email - emails = ActionMailer::Base.deliveries - assert !emails.empty?, "No emails were sent" - if block_given? - matching_emails = emails.select {|email| yield email } - assert !matching_emails.empty?, "None of the emails matched." - end - end - - # Asserts that no ActionMailer mails were delivered - # - # assert_did_not_send_email - def assert_did_not_send_email - msg = "Sent #{ActionMailer::Base.deliveries.size} emails.\n" - ActionMailer::Base.deliveries.each { |m| msg << " '#{m.subject}' sent to #{m.to.to_sentence}\n" } - assert ActionMailer::Base.deliveries.empty?, msg - end - - def pretty_error_messages(obj) - obj.errors.map { |a, m| "#{a} #{m} (#{obj.send(a).inspect})" } - end - end + + module ClassMethods + # Loads all fixture files (test/fixtures/*.yml) + def load_all_fixtures + all_fixtures = Dir.glob(File.join(Test::Unit::TestCase.fixture_path, "*.yml")).collect do |f| + File.basename(f, '.yml').to_sym + end + fixtures *all_fixtures + end + end + + # Prints a message to stdout, tagged with the name of the calling method. + def report!(msg = "") + puts("#{caller.first}: #{msg}") + 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 + # assert_contains(['a', '1'], 'a') => passes + # assert_contains(['a', '1'], /not there/) => fails + def assert_contains(collection, x, extra_msg = "") + collection = [collection] unless collection.is_a?(Array) + msg = "#{x.inspect} 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.inspect} 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 object can be saved + # + # assert_save User.new(params) + def assert_save(obj) + assert obj.save, "Errors: #{pretty_error_messages obj}" + obj.reload + end + + # Asserts that the given object is valid + # + # assert_valid User.new(params) + def assert_valid(obj) + assert obj.valid?, "Errors: #{pretty_error_messages obj}" + end + + # Asserts that an email was delivered. Can take a block that can further + # narrow down the types of emails you're expecting. + # + # assert_sent_email + # + # Passes if ActionMailer::Base.deliveries has an email + # + # assert_sent_email do |email| + # email.subject =~ /hi there/ && email.to.include?('none@none.com') + # end + # + # Passes if there is an email with subject containing 'hi there' and + # 'none@none.com' as one of the recipients. + # + def assert_sent_email + emails = ActionMailer::Base.deliveries + assert !emails.empty?, "No emails were sent" + if block_given? + matching_emails = emails.select {|email| yield email } + assert !matching_emails.empty?, "None of the emails matched." + end + end + + # Asserts that no ActionMailer mails were delivered + # + # assert_did_not_send_email + def assert_did_not_send_email + msg = "Sent #{ActionMailer::Base.deliveries.size} emails.\n" + ActionMailer::Base.deliveries.each { |m| msg << " '#{m.subject}' sent to #{m.to.to_sentence}\n" } + assert ActionMailer::Base.deliveries.empty?, msg + end + + def pretty_error_messages(obj) + obj.errors.map { |a, m| "#{a} #{m} (#{obj.send(a).inspect})" } + end + end end diff --git a/lib/shoulda/private_helpers.rb b/lib/shoulda/private_helpers.rb index 6c14a24d..e5799572 100644 --- a/lib/shoulda/private_helpers.rb +++ b/lib/shoulda/private_helpers.rb @@ -1,22 +1,20 @@ -module ThoughtBot # :nodoc: - module Shoulda # :nodoc: - module Private # :nodoc: - # Returns the values for the entries in the args hash who's keys are listed in the wanted array. - # Will raise if there are keys in the args hash that aren't listed. - def get_options!(args, *wanted) - ret = [] - opts = (args.last.is_a?(Hash) ? args.pop : {}) - wanted.each {|w| ret << opts.delete(w)} - raise ArgumentError, "Unsupported options given: #{opts.keys.join(', ')}" unless opts.keys.empty? - return *ret - end +module Shoulda # :nodoc: + module Private # :nodoc: + # Returns the values for the entries in the args hash who's keys are listed in the wanted array. + # Will raise if there are keys in the args hash that aren't listed. + def get_options!(args, *wanted) + ret = [] + opts = (args.last.is_a?(Hash) ? args.pop : {}) + wanted.each {|w| ret << opts.delete(w)} + raise ArgumentError, "Unsupported options given: #{opts.keys.join(', ')}" unless opts.keys.empty? + return *ret + end - # Returns the model class constant, as determined by the test class name. - # - # class TestUser; model_class; end => User - def model_class - self.name.gsub(/Test$/, '').constantize - end + # Returns the model class constant, as determined by the test class name. + # + # class TestUser; model_class; end => User + def model_class + self.name.gsub(/Test$/, '').constantize end end end diff --git a/test/other/private_helpers_test.rb b/test/other/private_helpers_test.rb index 9c359997..f41dc8ac 100644 --- a/test/other/private_helpers_test.rb +++ b/test/other/private_helpers_test.rb @@ -1,7 +1,7 @@ require File.join(File.dirname(__FILE__), '..', 'test_helper') class PrivateHelpersTest < Test::Unit::TestCase # :nodoc: - include ThoughtBot::Shoulda::ActiveRecord + include Shoulda::ActiveRecord context "get_options!" do should "remove opts from args" do args = [:a, :b, {}] From 804f859435a3ffa3012466a8c3f745fdc8b33965 Mon Sep 17 00:00:00 2001 From: Tammer Saleh Date: Sun, 22 Jun 2008 12:03:46 -0400 Subject: [PATCH 5/8] Rearranged a bunch of code --- Rakefile | 2 +- lib/shoulda.rb | 121 ++++++++- lib/shoulda/color.rb | 4 +- lib/shoulda/context.rb | 121 +++++++++ lib/shoulda/extensions/proc.rb | 17 ++ lib/shoulda/gem/proc_extensions.rb | 14 - lib/shoulda/gem/shoulda.rb | 239 ------------------ test/{other => shoulda}/context_test.rb | 6 - test/{other => shoulda}/helpers_test.rb | 0 .../private_helpers_test.rb | 0 test/shoulda/shoulda_test.rb | 236 +++++++++++++++++ 11 files changed, 498 insertions(+), 262 deletions(-) create mode 100644 lib/shoulda/context.rb create mode 100644 lib/shoulda/extensions/proc.rb delete mode 100644 lib/shoulda/gem/proc_extensions.rb delete mode 100644 lib/shoulda/gem/shoulda.rb rename test/{other => shoulda}/context_test.rb (85%) rename test/{other => shoulda}/helpers_test.rb (100%) rename test/{other => shoulda}/private_helpers_test.rb (100%) create mode 100644 test/shoulda/shoulda_test.rb diff --git a/Rakefile b/Rakefile index 52c47ecf..4bb9a7ea 100644 --- a/Rakefile +++ b/Rakefile @@ -6,7 +6,7 @@ require 'rake/rdoctask' Rake::TestTask.new do |t| t.libs << 'lib' - t.pattern = 'test/{unit,functional,other}/**/*_test.rb' + t.pattern = 'test/**/*_test.rb' t.verbose = false end diff --git a/lib/shoulda.rb b/lib/shoulda.rb index cd571df2..e581977e 100644 --- a/lib/shoulda.rb +++ b/lib/shoulda.rb @@ -1,4 +1,4 @@ -require 'shoulda/gem/shoulda' +require 'shoulda/context' require 'shoulda/private_helpers' require 'shoulda/general' require 'shoulda/active_record_helpers' @@ -22,6 +22,125 @@ end require 'shoulda/color' if shoulda_options[:color] +module Shoulda + class << self + attr_accessor :current_context + end + + VERSION = '1.1.1' + + # = Should statements + # + # Should statements are just syntactic sugar over normal Test::Unit test methods. A should block + # contains all the normal code and assertions you're used to seeing, with the added benefit that + # they can be wrapped inside context blocks (see below). + # + # == Example: + # + # class UserTest << Test::Unit::TestCase + # + # def setup + # @user = User.new("John", "Doe") + # end + # + # should "return its full name" + # assert_equal 'John Doe', @user.full_name + # end + # + # end + # + # ...will produce the following test: + # * "test: User should return its full name. " + # + # Note: The part before should in the test name is gleamed from the name of the Test::Unit class. + + def should(name, &blk) + should_eventually(name) && return unless block_given? + + if Shoulda.current_context + Shoulda.current_context.should(name, &blk) + else + context_name = self.name.gsub(/Test/, "") + context = Shoulda::Context.new(context_name, self) do + should(name, &blk) + end + context.build + end + end + + # Just like should, but never runs, and instead prints an 'X' in the Test::Unit output. + def should_eventually(name, &blk) + context_name = self.name.gsub(/Test/, "") + context = Shoulda::Context.new(context_name, self) do + should_eventually(name, &blk) + end + context.build + end + + # = Contexts + # + # A context block groups should statements under a common set of setup/teardown methods. + # Context blocks can be arbitrarily nested, and can do wonders for improving the maintainability + # and readability of your test code. + # + # A context block can contain setup, should, should_eventually, and teardown blocks. + # + # class UserTest << Test::Unit::TestCase + # 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 "test: A User instance should return its full name. ". + # + # Contexts may be nested. Nested contexts run their setup blocks from out to in before each + # should statement. They then run their teardown blocks from in to out after each should statement. + # + # class UserTest << Test::Unit::TestCase + # 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 + # * "test: A User instance should return its full name. " + # * "test: A User instance with a profile should return true when sent :has_profile?. " + # + # Just like should statements, a context block can exist next to normal def test_the_old_way; end + # tests. This means you do not have to fully commit to the context/should syntax in a test file. + + def context(name, &blk) + if Shoulda.current_context + Shoulda.current_context.context(name, &blk) + else + context = Shoulda::Context.new(name, self, &blk) + context.build + end + end +end + module Test # :nodoc: all module Unit class TestCase diff --git a/lib/shoulda/color.rb b/lib/shoulda/color.rb index 1324dd7b..170ae2c1 100644 --- a/lib/shoulda/color.rb +++ b/lib/shoulda/color.rb @@ -1,6 +1,8 @@ require 'test/unit/ui/console/testrunner' -# Completely stolen from redgreen gem +# Completely stolen from redgreen gem (thanks Pat Eyler and Chris Wanstrath). +# +# NOTE: Is this now in autotest? Can I remove this, then? # # Adds colored output to your tests. Specify color: true in # your ~/.shoulda.conf file to enable. diff --git a/lib/shoulda/context.rb b/lib/shoulda/context.rb new file mode 100644 index 00000000..333458ea --- /dev/null +++ b/lib/shoulda/context.rb @@ -0,0 +1,121 @@ +require File.join(File.dirname(__FILE__), 'extensions', 'proc') + +module Shoulda # :nodoc: + class Context + attr_accessor :name # my name + attr_accessor :parent # may be another context, or the original test::unit class. + attr_accessor :subcontexts # array of contexts nested under myself + attr_accessor :setup_block # block given via a setup method + attr_accessor :teardown_block # block given via a teardown method + attr_accessor :shoulds # array of hashes representing the should statements + attr_accessor :should_eventuallys # array of hashes representing the should eventually statements + + def initialize(name, parent, &blk) + Shoulda.current_context = self + self.name = name + self.parent = parent + self.setup_block = nil + self.teardown_block = nil + self.shoulds = [] + self.should_eventuallys = [] + self.subcontexts = [] + + blk.bind(self).call + Shoulda.current_context = nil + end + + def context(name, &blk) + subcontexts << Context.new(name, self, &blk) + Shoulda.current_context = self + end + + def setup(&blk) + self.setup_block = blk + end + + def teardown(&blk) + self.teardown_block = blk + end + + def should(name, &blk) + self.shoulds << { :name => name, :block => blk } + end + + def should_eventually(name, &blk) + self.should_eventuallys << { :name => name, :block => blk } + end + + def full_name + parent_name = parent.full_name if am_subcontext? + return [parent_name, name].join(" ").strip + end + + def am_subcontext? + parent.is_a?(self.class) # my parent is the same class as myself. + end + + def test_unit_class + am_subcontext? ? parent.test_unit_class : parent + end + + def create_test_from_should_hash(should) + test_name = ["test:", full_name, "should", "#{should[:name]}. "].flatten.join(' ').to_sym + + if test_unit_class.instance_methods.include?(test_name.to_s) + warn " * WARNING: '#{test_name}' is already defined" + end + + context = self + test_unit_class.send(:define_method, test_name) do |*args| + begin + context.run_all_setup_blocks(self) + should[:block].bind(self).call + ensure + context.run_all_teardown_blocks(self) + end + end + end + + def run_all_setup_blocks(binding) + self.parent.run_all_setup_blocks(binding) if am_subcontext? + setup_block.bind(binding).call if setup_block + end + + def run_all_teardown_blocks(binding) + teardown_block.bind(binding).call if teardown_block + self.parent.run_all_teardown_blocks(binding) if am_subcontext? + end + + def print_should_eventuallys + should_eventuallys.each do |should| + test_name = [full_name, "should", "#{should[:name]}. "].flatten.join(' ') + puts " * DEFERRED: " + test_name + end + subcontexts.each { |context| context.print_should_eventuallys } + end + + def build + shoulds.each do |should| + create_test_from_should_hash(should) + end + + subcontexts.each { |context| context.build } + + print_should_eventuallys + end + + def method_missing(method, *args, &blk) + test_unit_class.send(method, *args, &blk) + end + + end +end + +module Test # :nodoc: all + module Unit + class TestCase + extend Shoulda + end + end +end + diff --git a/lib/shoulda/extensions/proc.rb b/lib/shoulda/extensions/proc.rb new file mode 100644 index 00000000..ec2c7d88 --- /dev/null +++ b/lib/shoulda/extensions/proc.rb @@ -0,0 +1,17 @@ +begin + require 'active_support' +rescue LoadError + # Stolen straight from ActiveSupport + class Proc #:nodoc: + def bind(object) + block, time = self, Time.now + (class << object; self end).class_eval do + method_name = "__bind_#{time.to_i}_#{time.usec}" + define_method(method_name, &block) + method = instance_method(method_name) + remove_method(method_name) + method + end.bind(object) + end + end +end diff --git a/lib/shoulda/gem/proc_extensions.rb b/lib/shoulda/gem/proc_extensions.rb deleted file mode 100644 index 0d577df9..00000000 --- a/lib/shoulda/gem/proc_extensions.rb +++ /dev/null @@ -1,14 +0,0 @@ -# Stolen straight from ActiveSupport - -class Proc #:nodoc: - def bind(object) - block, time = self, Time.now - (class << object; self end).class_eval do - method_name = "__bind_#{time.to_i}_#{time.usec}" - define_method(method_name, &block) - method = instance_method(method_name) - remove_method(method_name) - method - end.bind(object) - end -end diff --git a/lib/shoulda/gem/shoulda.rb b/lib/shoulda/gem/shoulda.rb deleted file mode 100644 index 19163089..00000000 --- a/lib/shoulda/gem/shoulda.rb +++ /dev/null @@ -1,239 +0,0 @@ -require File.join(File.dirname(__FILE__), 'proc_extensions') - -module Shoulda - class << self - attr_accessor :current_context - end - - VERSION = '1.1.1' - - # = Should statements - # - # Should statements are just syntactic sugar over normal Test::Unit test methods. A should block - # contains all the normal code and assertions you're used to seeing, with the added benefit that - # they can be wrapped inside context blocks (see below). - # - # == Example: - # - # class UserTest << Test::Unit::TestCase - # - # def setup - # @user = User.new("John", "Doe") - # end - # - # should "return its full name" - # assert_equal 'John Doe', @user.full_name - # end - # - # end - # - # ...will produce the following test: - # * "test: User should return its full name. " - # - # Note: The part before should in the test name is gleamed from the name of the Test::Unit class. - - def should(name, &blk) - should_eventually(name) && return unless block_given? - - if Shoulda.current_context - Shoulda.current_context.should(name, &blk) - else - context_name = self.name.gsub(/Test/, "") - context = Shoulda::Context.new(context_name, self) do - should(name, &blk) - end - context.build - end - end - - # Just like should, but never runs, and instead prints an 'X' in the Test::Unit output. - def should_eventually(name, &blk) - context_name = self.name.gsub(/Test/, "") - context = Shoulda::Context.new(context_name, self) do - should_eventually(name, &blk) - end - context.build - end - - # = Contexts - # - # A context block groups should statements under a common set of setup/teardown methods. - # Context blocks can be arbitrarily nested, and can do wonders for improving the maintainability - # and readability of your test code. - # - # A context block can contain setup, should, should_eventually, and teardown blocks. - # - # class UserTest << Test::Unit::TestCase - # 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 "test: A User instance should return its full name. ". - # - # Contexts may be nested. Nested contexts run their setup blocks from out to in before each - # should statement. They then run their teardown blocks from in to out after each should statement. - # - # class UserTest << Test::Unit::TestCase - # 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 - # * "test: A User instance should return its full name. " - # * "test: A User instance with a profile should return true when sent :has_profile?. " - # - # Just like should statements, a context block can exist next to normal def test_the_old_way; end - # tests. This means you do not have to fully commit to the context/should syntax in a test file. - - def context(name, &blk) - if Shoulda.current_context - Shoulda.current_context.context(name, &blk) - else - context = Shoulda::Context.new(name, self, &blk) - context.build - end - end - - class Context # :nodoc: - - attr_accessor :name # my name - attr_accessor :parent # may be another context, or the original test::unit class. - attr_accessor :subcontexts # array of contexts nested under myself - attr_accessor :setup_block # block given via a setup method - attr_accessor :teardown_block # block given via a teardown method - attr_accessor :shoulds # array of hashes representing the should statements - attr_accessor :should_eventuallys # array of hashes representing the should eventually statements - - def initialize(name, parent, &blk) - Shoulda.current_context = self - self.name = name - self.parent = parent - self.setup_block = nil - self.teardown_block = nil - self.shoulds = [] - self.should_eventuallys = [] - self.subcontexts = [] - - blk.bind(self).call - Shoulda.current_context = nil - end - - def context(name, &blk) - subcontexts << Context.new(name, self, &blk) - Shoulda.current_context = self - end - - def setup(&blk) - self.setup_block = blk - end - - def teardown(&blk) - self.teardown_block = blk - end - - def should(name, &blk) - self.shoulds << { :name => name, :block => blk } - end - - def should_eventually(name, &blk) - self.should_eventuallys << { :name => name, :block => blk } - end - - def full_name - parent_name = parent.full_name if am_subcontext? - return [parent_name, name].join(" ").strip - end - - def am_subcontext? - parent.is_a?(self.class) # my parent is the same class as myself. - end - - def test_unit_class - am_subcontext? ? parent.test_unit_class : parent - end - - def create_test_from_should_hash(should) - test_name = ["test:", full_name, "should", "#{should[:name]}. "].flatten.join(' ').to_sym - - if test_unit_class.instance_methods.include?(test_name.to_s) - warn " * WARNING: '#{test_name}' is already defined" - end - - context = self - test_unit_class.send(:define_method, test_name) do |*args| - begin - context.run_all_setup_blocks(self) - should[:block].bind(self).call - ensure - context.run_all_teardown_blocks(self) - end - end - end - - def run_all_setup_blocks(binding) - self.parent.run_all_setup_blocks(binding) if am_subcontext? - setup_block.bind(binding).call if setup_block - end - - def run_all_teardown_blocks(binding) - teardown_block.bind(binding).call if teardown_block - self.parent.run_all_teardown_blocks(binding) if am_subcontext? - end - - def print_should_eventuallys - should_eventuallys.each do |should| - test_name = [full_name, "should", "#{should[:name]}. "].flatten.join(' ') - puts " * DEFERRED: " + test_name - end - subcontexts.each { |context| context.print_should_eventuallys } - end - - def build - shoulds.each do |should| - create_test_from_should_hash(should) - end - - subcontexts.each { |context| context.build } - - print_should_eventuallys - end - - def method_missing(method, *args, &blk) - test_unit_class.send(method, *args, &blk) - end - - end -end - -module Test # :nodoc: all - module Unit - class TestCase - extend Shoulda - end - end -end - diff --git a/test/other/context_test.rb b/test/shoulda/context_test.rb similarity index 85% rename from test/other/context_test.rb rename to test/shoulda/context_test.rb index 2a17e249..2000e3d1 100644 --- a/test/other/context_test.rb +++ b/test/shoulda/context_test.rb @@ -64,10 +64,4 @@ class ContextTest < Test::Unit::TestCase # :nodoc: end end - should_eventually "should pass, since it's unimplemented" do - flunk "what?" - end - - should_eventually "should not require a block when using should_eventually" - should "should pass without a block, as that causes it to piggyback to should_eventually" end diff --git a/test/other/helpers_test.rb b/test/shoulda/helpers_test.rb similarity index 100% rename from test/other/helpers_test.rb rename to test/shoulda/helpers_test.rb diff --git a/test/other/private_helpers_test.rb b/test/shoulda/private_helpers_test.rb similarity index 100% rename from test/other/private_helpers_test.rb rename to test/shoulda/private_helpers_test.rb diff --git a/test/shoulda/shoulda_test.rb b/test/shoulda/shoulda_test.rb new file mode 100644 index 00000000..bd26e06a --- /dev/null +++ b/test/shoulda/shoulda_test.rb @@ -0,0 +1,236 @@ +require 'test/unit' +require 'rubygems' +require 'mocha' + +class ShouldaTest < Test::Unit::TestCase # :nodoc: + + should "be able to define a should statement outside of a context" do + assert true + end + + should "see the name of my class as ShouldaTest" do + assert_equal "ShouldaTest", self.class.name + end + + def self.should_see_class_methods + should "be able to see class methods" do + assert true + end + end + + def self.should_see_a_context_block_like_a_Test_Unit_class + should "see a context block as a Test::Unit class" do + assert_equal "ShouldaTest", self.class.name + end + end + + def self.should_see_blah + should "see @blah through a macro" do + assert @blah + end + end + + def self.should_not_see_blah + should "not see @blah through a macro" do + assert_nil @blah + end + end + + def self.should_be_able_to_make_context_macros(prefix = nil) + context "a macro" do + should "have the tests named correctly" do + assert_match(/^test: #{prefix}a macro should have the tests named correctly/, self.to_s) + end + end + end + + context "Context" do + + should_see_class_methods + should_see_a_context_block_like_a_Test_Unit_class + should_be_able_to_make_context_macros("Context ") + + should "not define @blah" do + assert ! self.instance_variables.include?("@blah") + end + + should_not_see_blah + + should "be able to define a should statement" do + assert true + end + + should "see the name of my class as ShouldaTest" do + assert_equal "ShouldaTest", self.class.name + end + + context "with a subcontext" do + should_be_able_to_make_context_macros("Context with a subcontext ") + end + end + + context "Context with setup block" do + setup do + @blah = "blah" + end + + should "have @blah == 'blah'" do + assert_equal "blah", @blah + end + should_see_blah + + should "have name set right" do + assert_match(/^test: Context with setup block/, self.to_s) + end + + context "and a subcontext" do + setup do + @blah = "#{@blah} twice" + end + + should "be named correctly" do + assert_match(/^test: Context with setup block and a subcontext should be named correctly/, self.to_s) + end + + should "run the setup methods in order" do + assert_equal @blah, "blah twice" + end + should_see_blah + end + end + + context "Another context with setup block" do + setup do + @blah = "foo" + end + + should "have @blah == 'foo'" do + assert_equal "foo", @blah + end + + should "have name set right" do + assert_match(/^test: Another context with setup block/, self.to_s) + end + should_see_blah + end + + should_eventually "pass, since it's a should_eventually" do + flunk "what?" + end + + should_eventually "should not require a block when using should_eventually" + should "should pass without a block, as that causes it to piggyback to should_eventually" + + # Context creation and naming + + def test_should_create_a_new_context + assert_nothing_raised do + Shoulda::Context.new("context name", self) do; end + end + end + + def test_should_create_a_nested_context + assert_nothing_raised do + parent = Shoulda::Context.new("Parent", self) do; end + child = Shoulda::Context.new("Child", parent) do; end + end + end + + def test_should_name_a_contexts_correctly + parent = Shoulda::Context.new("Parent", self) do; end + child = Shoulda::Context.new("Child", parent) do; end + grandchild = Shoulda::Context.new("GrandChild", child) do; end + + assert_equal "Parent", parent.full_name + assert_equal "Parent Child", child.full_name + assert_equal "Parent Child GrandChild", grandchild.full_name + end + + # Should statements + + def test_should_have_should_hashes_when_given_should_statements + context = Shoulda::Context.new("name", self) do + should "be good" do; end + should "another" do; end + end + + names = context.shoulds.map {|s| s[:name]} + assert_equal ["another", "be good"], names.sort + end + + # setup and teardown + + def test_should_capture_setup_and_teardown_blocks + context = Shoulda::Context.new("name", self) do + setup do; "setup"; end + teardown do; "teardown"; end + end + + assert_equal "setup", context.setup_block.call + assert_equal "teardown", context.teardown_block.call + end + + # building + + def test_should_create_shoulda_test_for_each_should_on_build + context = Shoulda::Context.new("name", self) do + should "one" do; end + should "two" do; end + end + context.expects(:create_test_from_should_hash).with(has_entry(:name => "one")) + context.expects(:create_test_from_should_hash).with(has_entry(:name => "two")) + context.build + end + + def test_should_create_test_methods_on_build + tu_class = Test::Unit::TestCase + context = Shoulda::Context.new("A Context", tu_class) do + should "define the test" do; end + end + + tu_class.expects(:define_method).with(:"test: A Context should define the test. ") + context.build + end + + def test_should_create_test_methods_on_build_when_subcontext + tu_class = Test::Unit::TestCase + context = Shoulda::Context.new("A Context", tu_class) do + context "with a child" do + should "define the test" do; end + end + end + + tu_class.expects(:define_method).with(:"test: A Context with a child should define the test. ") + context.build + end + + # Test::Unit integration + + def test_should_create_a_new_context_and_build_it_on_Test_Unit_context + c = mock("context") + c.expects(:build) + Shoulda::Context.expects(:new).with("foo", kind_of(Class)).returns(c) + self.class.context "foo" do; end + end + + def test_should_create_a_one_off_context_and_build_it_on_Test_Unit_should + s = mock("test") + Shoulda::Context.any_instance.expects(:should).with("rock").returns(s) + Shoulda::Context.any_instance.expects(:build) + self.class.should "rock" do; end + end + + def test_should_define_a_test_on_should + s = mock("test") + Shoulda::Context.any_instance.expects(:should).with("rock").returns(s) + Shoulda::Context.any_instance.expects(:build) + self.class.should "rock" do; end + end + + def test_should_create_a_one_off_context_and_build_it_on_Test_Unit_should_eventually + s = mock("test") + Shoulda::Context.any_instance.expects(:should_eventually).with("rock").returns(s) + Shoulda::Context.any_instance.expects(:build) + self.class.should_eventually "rock" do; end + end +end From 35213b5a1b84c58064d8a13b50716b29a205ebb4 Mon Sep 17 00:00:00 2001 From: Tammer Saleh Date: Sun, 22 Jun 2008 13:35:50 -0400 Subject: [PATCH 6/8] documentation fixes --- lib/shoulda.rb | 12 +---- lib/shoulda/active_record_helpers.rb | 2 +- lib/shoulda/context.rb | 49 +++++++++++++------ .../controller_tests/controller_tests.rb | 2 +- lib/shoulda/controller_tests/formats/html.rb | 2 +- lib/shoulda/controller_tests/formats/xml.rb | 2 +- lib/shoulda/general.rb | 2 +- lib/shoulda/private_helpers.rb | 2 +- 8 files changed, 42 insertions(+), 31 deletions(-) diff --git a/lib/shoulda.rb b/lib/shoulda.rb index e581977e..b198c300 100644 --- a/lib/shoulda.rb +++ b/lib/shoulda.rb @@ -27,10 +27,6 @@ module Shoulda attr_accessor :current_context end - VERSION = '1.1.1' - - # = Should statements - # # Should statements are just syntactic sugar over normal Test::Unit test methods. A should block # contains all the normal code and assertions you're used to seeing, with the added benefit that # they can be wrapped inside context blocks (see below). @@ -68,7 +64,7 @@ module Shoulda end end - # Just like should, but never runs, and instead prints an 'X' in the Test::Unit output. + # Just like should, but never runs, and instead prints a differed message in the Test::Unit output. def should_eventually(name, &blk) context_name = self.name.gsub(/Test/, "") context = Shoulda::Context.new(context_name, self) do @@ -77,8 +73,6 @@ module Shoulda context.build end - # = Contexts - # # A context block groups should statements under a common set of setup/teardown methods. # Context blocks can be arbitrarily nested, and can do wonders for improving the maintainability # and readability of your test code. @@ -144,11 +138,9 @@ end module Test # :nodoc: all module Unit class TestCase - include Shoulda::General include Shoulda::Controller - - extend Shoulda::ActiveRecord + extend Shoulda::ActiveRecord end end end diff --git a/lib/shoulda/active_record_helpers.rb b/lib/shoulda/active_record_helpers.rb index 15639864..6b6308cb 100644 --- a/lib/shoulda/active_record_helpers.rb +++ b/lib/shoulda/active_record_helpers.rb @@ -1,4 +1,4 @@ -module Shoulda # :nodoc: +module Shoulda # = Macro test helpers for your active record models # # These helpers will test most of the validations and associations for your ActiveRecord models. diff --git a/lib/shoulda/context.rb b/lib/shoulda/context.rb index 333458ea..3520ae5c 100644 --- a/lib/shoulda/context.rb +++ b/lib/shoulda/context.rb @@ -1,6 +1,6 @@ require File.join(File.dirname(__FILE__), 'extensions', 'proc') -module Shoulda # :nodoc: +module Shoulda class Context attr_accessor :name # my name attr_accessor :parent # may be another context, or the original test::unit class. @@ -24,40 +24,53 @@ module Shoulda # :nodoc: Shoulda.current_context = nil end + # Creates a context. See Shoulda#context. def context(name, &blk) subcontexts << Context.new(name, self, &blk) Shoulda.current_context = self end - def setup(&blk) - self.setup_block = blk - end - - def teardown(&blk) - self.teardown_block = blk - end - + # Creates a should statement. See Shoulda#should. def should(name, &blk) self.shoulds << { :name => name, :block => blk } end + # Creates a should_eventually statement. See Shoulda#should_eventually. def should_eventually(name, &blk) self.should_eventuallys << { :name => name, :block => blk } end + # Any code in a setup block will be run before the should statements in a + # context. Nested contexts will have their setup blocks run in order. + def setup(&blk) + self.setup_block = blk + end + + # Any code in a teardown block will be run after the should statements in a + # context. Nested contexts will have their teardown blocks run in reverse + # order. + def teardown(&blk) + self.teardown_block = blk + end + + # The full name of this context, including parents. def full_name - parent_name = parent.full_name if am_subcontext? + parent_name = parent.full_name if subcontext? return [parent_name, name].join(" ").strip end - def am_subcontext? + # Returns true if this context is nested + def subcontext? parent.is_a?(self.class) # my parent is the same class as myself. end + # Returns the root class that decends from Test::Unit. def test_unit_class - am_subcontext? ? parent.test_unit_class : parent + subcontext? ? parent.test_unit_class : parent end + + # Creates a single test from a should hash def create_test_from_should_hash(should) test_name = ["test:", full_name, "should", "#{should[:name]}. "].flatten.join(' ').to_sym @@ -76,16 +89,19 @@ module Shoulda # :nodoc: end end + # Runs all the setup blocks in order def run_all_setup_blocks(binding) - self.parent.run_all_setup_blocks(binding) if am_subcontext? + self.parent.run_all_setup_blocks(binding) if subcontext? setup_block.bind(binding).call if setup_block end + # Runs all the teardown blocks in reverse order def run_all_teardown_blocks(binding) teardown_block.bind(binding).call if teardown_block - self.parent.run_all_teardown_blocks(binding) if am_subcontext? + self.parent.run_all_teardown_blocks(binding) if subcontext? end + # Prints the should_eventually names to stdout def print_should_eventuallys should_eventuallys.each do |should| test_name = [full_name, "should", "#{should[:name]}. "].flatten.join(' ') @@ -94,6 +110,7 @@ module Shoulda # :nodoc: subcontexts.each { |context| context.print_should_eventuallys } end + # Triggers the test method creation process, and prints the unimplemented tests. def build shoulds.each do |should| create_test_from_should_hash(should) @@ -104,10 +121,12 @@ module Shoulda # :nodoc: print_should_eventuallys end + # This delegates all method calls inside a context to the surrounding + # Test::Unit class. This allows us to call Test::Unit macros inside a + # context. def method_missing(method, *args, &blk) test_unit_class.send(method, *args, &blk) end - end end diff --git a/lib/shoulda/controller_tests/controller_tests.rb b/lib/shoulda/controller_tests/controller_tests.rb index c5b1d5d2..ef2d5247 100644 --- a/lib/shoulda/controller_tests/controller_tests.rb +++ b/lib/shoulda/controller_tests/controller_tests.rb @@ -1,4 +1,4 @@ -module Shoulda # :nodoc: +module Shoulda module Controller def self.included(other) # :nodoc: other.class_eval do diff --git a/lib/shoulda/controller_tests/formats/html.rb b/lib/shoulda/controller_tests/formats/html.rb index 1f77398f..d962e031 100644 --- a/lib/shoulda/controller_tests/formats/html.rb +++ b/lib/shoulda/controller_tests/formats/html.rb @@ -1,4 +1,4 @@ -module Shoulda # :nodoc: +module Shoulda module Controller # :nodoc: module HTML # :nodoc: all def self.included(other) diff --git a/lib/shoulda/controller_tests/formats/xml.rb b/lib/shoulda/controller_tests/formats/xml.rb index c8c97fdb..5fb4fbcd 100644 --- a/lib/shoulda/controller_tests/formats/xml.rb +++ b/lib/shoulda/controller_tests/formats/xml.rb @@ -1,4 +1,4 @@ -module Shoulda # :nodoc: +module Shoulda module Controller # :nodoc: module XML def self.included(other) #:nodoc: diff --git a/lib/shoulda/general.rb b/lib/shoulda/general.rb index 16f0e2cb..ec350319 100644 --- a/lib/shoulda/general.rb +++ b/lib/shoulda/general.rb @@ -1,4 +1,4 @@ -module Shoulda # :nodoc: +module Shoulda module General def self.included(other) # :nodoc: other.class_eval do diff --git a/lib/shoulda/private_helpers.rb b/lib/shoulda/private_helpers.rb index e5799572..a7d8ecb6 100644 --- a/lib/shoulda/private_helpers.rb +++ b/lib/shoulda/private_helpers.rb @@ -1,4 +1,4 @@ -module Shoulda # :nodoc: +module Shoulda module Private # :nodoc: # Returns the values for the entries in the args hash who's keys are listed in the wanted array. # Will raise if there are keys in the args hash that aren't listed. From 2442d1f6087041c18a2124bce4ea8a19599cde0f Mon Sep 17 00:00:00 2001 From: Tammer Saleh Date: Sun, 22 Jun 2008 14:11:40 -0400 Subject: [PATCH 7/8] Cleaned up the config file logic and now only loading rails parts when the constants are already there. --- lib/shoulda.rb | 32 +++++++------------ .../controller_tests/controller_tests.rb | 8 +++++ 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/lib/shoulda.rb b/lib/shoulda.rb index b198c300..56801971 100644 --- a/lib/shoulda.rb +++ b/lib/shoulda.rb @@ -1,23 +1,20 @@ require 'shoulda/context' require 'shoulda/private_helpers' require 'shoulda/general' -require 'shoulda/active_record_helpers' -require 'shoulda/controller_tests/controller_tests.rb' require 'yaml' +require 'shoulda/active_record_helpers' if defined?(ActiveRecord) +require 'shoulda/controller_tests/controller_tests.rb' if defined?(ActionController) + +config_files = [] +config_files << "shoulda.conf" +config_files << File.join("test", "shoulda.conf") +config_files << File.join(RAILS_ROOT, "test", "shoulda.conf") if defined?(RAILS_ROOT) +config_files << File.join(ENV["HOME"], ".shoulda.conf") if ENV["HOME"] + shoulda_options = {} - -possible_config_paths = [] -possible_config_paths << File.join(ENV["HOME"], ".shoulda.conf") if ENV["HOME"] -possible_config_paths << "shoulda.conf" -possible_config_paths << File.join("test", "shoulda.conf") -possible_config_paths << File.join(RAILS_ROOT, "test", "shoulda.conf") if defined?(RAILS_ROOT) - -possible_config_paths.each do |config_file| - if File.exists? config_file - shoulda_options = YAML.load_file(config_file).symbolize_keys - break - end +config_files.each do |file| + shoulda_options.merge!(YAML.load_file(file).symbolize_keys) if File.exists? file end require 'shoulda/color' if shoulda_options[:color] @@ -145,10 +142,3 @@ module Test # :nodoc: all end end -module ActionController #:nodoc: all - module Integration - class Session - include Shoulda::General - end - end -end diff --git a/lib/shoulda/controller_tests/controller_tests.rb b/lib/shoulda/controller_tests/controller_tests.rb index ef2d5247..7478f026 100644 --- a/lib/shoulda/controller_tests/controller_tests.rb +++ b/lib/shoulda/controller_tests/controller_tests.rb @@ -462,3 +462,11 @@ module Shoulda end end end + +module ActionController #:nodoc: all + module Integration + class Session + include Shoulda::General + end + end +end From b415bff884a3c25a058c2600f86da80d4fa28fde Mon Sep 17 00:00:00 2001 From: Tammer Saleh Date: Mon, 23 Jun 2008 12:36:31 -0400 Subject: [PATCH 8/8] Revert bunch of bad commits: 2442d1f6087041c18a2124bce4ea8a19599cde0f 35213b5a1b84c58064d8a13b50716b29a205ebb4 804f859435a3ffa3012466a8c3f745fdc8b33965 fc938bb18557c1f5a801e3e2a15c9e4e9f8f03db Getting the following error when running tests in a real project: ...activesupport/lib/active_support/dependencies.rb:276:in `load_missing_constant': uninitialized constant Shoulda::ActiveRecord::Base (NameError) Think this is because of the namespace changes, but not sure. Will investigate later. --- README.rdoc | 8 +- Rakefile | 2 +- lib/shoulda.rb | 155 +-- lib/shoulda/active_record_helpers.rb | 1052 +++++++++-------- lib/shoulda/color.rb | 22 +- lib/shoulda/context.rb | 140 --- .../controller_tests/controller_tests.rb | 841 +++++++------ lib/shoulda/controller_tests/formats/html.rb | 348 +++--- lib/shoulda/controller_tests/formats/xml.rb | 304 ++--- lib/shoulda/extensions/proc.rb | 17 - lib/shoulda/gem/proc_extensions.rb | 14 + lib/shoulda/gem/shoulda.rb | 241 ++++ lib/shoulda/general.rb | 210 ++-- lib/shoulda/private_helpers.rb | 34 +- test/{shoulda => other}/context_test.rb | 6 + test/{shoulda => other}/helpers_test.rb | 0 .../private_helpers_test.rb | 2 +- test/shoulda/shoulda_test.rb | 236 ---- 18 files changed, 1701 insertions(+), 1931 deletions(-) delete mode 100644 lib/shoulda/context.rb delete mode 100644 lib/shoulda/extensions/proc.rb create mode 100644 lib/shoulda/gem/proc_extensions.rb create mode 100644 lib/shoulda/gem/shoulda.rb rename test/{shoulda => other}/context_test.rb (85%) rename test/{shoulda => other}/helpers_test.rb (100%) rename test/{shoulda => other}/private_helpers_test.rb (93%) delete mode 100644 test/shoulda/shoulda_test.rb diff --git a/README.rdoc b/README.rdoc index 80f8171d..13f7a008 100644 --- a/README.rdoc +++ b/README.rdoc @@ -10,7 +10,7 @@ Assertions:: Many common rails testing idioms have been distilled into a set of = Usage -=== Context Helpers (Shoulda::Context) +=== Context Helpers (ThoughtBot::Shoulda::Context) Stop killing your fingers with all of those underscores... Name your tests with plain sentences! @@ -43,7 +43,7 @@ Produces the following test methods: So readable! -=== ActiveRecord Tests (Shoulda::ActiveRecord) +=== ActiveRecord Tests (ThoughtBot::Shoulda::ActiveRecord) Quick macro tests for your ActiveRecord associations and validations: @@ -73,7 +73,7 @@ Quick macro tests for your ActiveRecord associations and validations: Makes TDD so much easier. -=== Controller Tests (Shoulda::Controller::ClassMethods) +=== Controller Tests (ThoughtBot::Shoulda::Controller::ClassMethods) Macros to test the most common controller patterns... @@ -105,7 +105,7 @@ Test entire controllers in a few lines... should_be_restful generates 40 tests on the fly, for both html and xml requests. -=== Helpful Assertions (Shoulda::General) +=== Helpful Assertions (ThoughtBot::Shoulda::General) More to come here, but have fun with what's there. diff --git a/Rakefile b/Rakefile index 4bb9a7ea..52c47ecf 100644 --- a/Rakefile +++ b/Rakefile @@ -6,7 +6,7 @@ require 'rake/rdoctask' Rake::TestTask.new do |t| t.libs << 'lib' - t.pattern = 'test/**/*_test.rb' + t.pattern = 'test/{unit,functional,other}/**/*_test.rb' t.verbose = false end diff --git a/lib/shoulda.rb b/lib/shoulda.rb index 56801971..a90ff183 100644 --- a/lib/shoulda.rb +++ b/lib/shoulda.rb @@ -1,144 +1,43 @@ -require 'shoulda/context' +require 'shoulda/gem/shoulda' require 'shoulda/private_helpers' require 'shoulda/general' +require 'shoulda/active_record_helpers' +require 'shoulda/controller_tests/controller_tests.rb' require 'yaml' -require 'shoulda/active_record_helpers' if defined?(ActiveRecord) -require 'shoulda/controller_tests/controller_tests.rb' if defined?(ActionController) - -config_files = [] -config_files << "shoulda.conf" -config_files << File.join("test", "shoulda.conf") -config_files << File.join(RAILS_ROOT, "test", "shoulda.conf") if defined?(RAILS_ROOT) -config_files << File.join(ENV["HOME"], ".shoulda.conf") if ENV["HOME"] - shoulda_options = {} -config_files.each do |file| - shoulda_options.merge!(YAML.load_file(file).symbolize_keys) if File.exists? file + +possible_config_paths = [] +possible_config_paths << File.join(ENV["HOME"], ".shoulda.conf") if ENV["HOME"] +possible_config_paths << "shoulda.conf" +possible_config_paths << File.join("test", "shoulda.conf") +possible_config_paths << File.join(RAILS_ROOT, "test", "shoulda.conf") if defined?(RAILS_ROOT) + +possible_config_paths.each do |config_file| + if File.exists? config_file + shoulda_options = YAML.load_file(config_file).symbolize_keys + break + end end require 'shoulda/color' if shoulda_options[:color] -module Shoulda - class << self - attr_accessor :current_context - end - - # Should statements are just syntactic sugar over normal Test::Unit test methods. A should block - # contains all the normal code and assertions you're used to seeing, with the added benefit that - # they can be wrapped inside context blocks (see below). - # - # == Example: - # - # class UserTest << Test::Unit::TestCase - # - # def setup - # @user = User.new("John", "Doe") - # end - # - # should "return its full name" - # assert_equal 'John Doe', @user.full_name - # end - # - # end - # - # ...will produce the following test: - # * "test: User should return its full name. " - # - # Note: The part before should in the test name is gleamed from the name of the Test::Unit class. - - def should(name, &blk) - should_eventually(name) && return unless block_given? - - if Shoulda.current_context - Shoulda.current_context.should(name, &blk) - else - context_name = self.name.gsub(/Test/, "") - context = Shoulda::Context.new(context_name, self) do - should(name, &blk) - end - context.build - end - end - - # Just like should, but never runs, and instead prints a differed message in the Test::Unit output. - def should_eventually(name, &blk) - context_name = self.name.gsub(/Test/, "") - context = Shoulda::Context.new(context_name, self) do - should_eventually(name, &blk) - end - context.build - end - - # A context block groups should statements under a common set of setup/teardown methods. - # Context blocks can be arbitrarily nested, and can do wonders for improving the maintainability - # and readability of your test code. - # - # A context block can contain setup, should, should_eventually, and teardown blocks. - # - # class UserTest << Test::Unit::TestCase - # 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 "test: A User instance should return its full name. ". - # - # Contexts may be nested. Nested contexts run their setup blocks from out to in before each - # should statement. They then run their teardown blocks from in to out after each should statement. - # - # class UserTest << Test::Unit::TestCase - # 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 - # * "test: A User instance should return its full name. " - # * "test: A User instance with a profile should return true when sent :has_profile?. " - # - # Just like should statements, a context block can exist next to normal def test_the_old_way; end - # tests. This means you do not have to fully commit to the context/should syntax in a test file. - - def context(name, &blk) - if Shoulda.current_context - Shoulda.current_context.context(name, &blk) - else - context = Shoulda::Context.new(name, self, &blk) - context.build - end - end -end - module Test # :nodoc: all module Unit class TestCase - include Shoulda::General - include Shoulda::Controller - extend Shoulda::ActiveRecord + + include ThoughtBot::Shoulda::General + include ThoughtBot::Shoulda::Controller + + extend ThoughtBot::Shoulda::ActiveRecord end end end +module ActionController #:nodoc: all + module Integration + class Session + include ThoughtBot::Shoulda::General + end + end +end diff --git a/lib/shoulda/active_record_helpers.rb b/lib/shoulda/active_record_helpers.rb index 6b6308cb..ec840571 100644 --- a/lib/shoulda/active_record_helpers.rb +++ b/lib/shoulda/active_record_helpers.rb @@ -1,602 +1,604 @@ -module Shoulda - # = 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. - # - module ActiveRecord - # Ensures that the model cannot be saved if one of the attributes listed is not present. +module ThoughtBot # :nodoc: + module Shoulda # :nodoc: + # = Macro test helpers for your active record models # - # Options: - # * :message - value the test expects to find in errors.on(:attribute). - # Regexp or string. Default = /blank/ + # These helpers will test most of the validations and associations for your ActiveRecord models. # - # Example: - # should_require_attributes :name, :phone_number + # 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 # - def should_require_attributes(*attributes) - message = get_options!(attributes, :message) - message ||= /blank/ - klass = model_class - - attributes.each do |attribute| - should "require #{attribute} to be set" do - object = klass.new - object.send("#{attribute}=", nil) - assert !object.valid?, "#{klass.name} does not require #{attribute}." - assert object.errors.on(attribute), "#{klass.name} does not require #{attribute}." - assert_contains(object.errors.on(attribute), message) + # 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. + # + # Options: + # * :message - value the test expects to find in errors.on(:attribute). + # Regexp or string. Default = /blank/ + # + # Example: + # should_require_attributes :name, :phone_number + # + def should_require_attributes(*attributes) + message = get_options!(attributes, :message) + message ||= /blank/ + klass = model_class + + attributes.each do |attribute| + should "require #{attribute} to be set" do + object = klass.new + object.send("#{attribute}=", nil) + assert !object.valid?, "#{klass.name} does not require #{attribute}." + assert object.errors.on(attribute), "#{klass.name} does not require #{attribute}." + assert_contains(object.errors.on(attribute), message) + end end end - 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] - # - def should_require_unique_attributes(*attributes) - message, scope = get_options!(attributes, :message, :scoped_to) - scope = [*scope].compact - message ||= /taken/ - - klass = model_class - attributes.each do |attribute| - attribute = attribute.to_sym - should "require unique value for #{attribute}#{" scoped to #{scope.join(', ')}" if scope}" do - assert existing = klass.find(:first), "Can't find first #{klass}" - object = klass.new - - object.send(:"#{attribute}=", existing.send(attribute)) - if !scope.blank? - scope.each do |s| - assert_respond_to object, :"#{s}=", "#{klass.name} doesn't seem to have a #{s} attribute." - object.send(:"#{s}=", existing.send(s)) - end - end - - assert !object.valid?, "#{klass.name} does not require a unique value for #{attribute}." - assert object.errors.on(attribute), "#{klass.name} does not require a unique value for #{attribute}." - - assert_contains(object.errors.on(attribute), message) - - # Now test that the object is valid when changing the scoped attribute - # TODO: There is a chance that we could change the scoped field - # to a value that's already taken. An alternative implementation - # could actually find all values for scope and create a unique - # one. - if !scope.blank? - scope.each do |s| - # Assume the scope is a foreign key if the field is nil - object.send(:"#{s}=", existing.send(s).nil? ? 1 : existing.send(s).next) + # 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] + # + def should_require_unique_attributes(*attributes) + message, scope = get_options!(attributes, :message, :scoped_to) + scope = [*scope].compact + message ||= /taken/ + + klass = model_class + attributes.each do |attribute| + attribute = attribute.to_sym + should "require unique value for #{attribute}#{" scoped to #{scope.join(', ')}" if scope}" do + assert existing = klass.find(:first), "Can't find first #{klass}" + object = klass.new + + object.send(:"#{attribute}=", existing.send(attribute)) + if !scope.blank? + scope.each do |s| + assert_respond_to object, :"#{s}=", "#{klass.name} doesn't seem to have a #{s} attribute." + object.send(:"#{s}=", existing.send(s)) + end end + + assert !object.valid?, "#{klass.name} does not require a unique value for #{attribute}." + assert object.errors.on(attribute), "#{klass.name} does not require a unique value for #{attribute}." + + assert_contains(object.errors.on(attribute), message) + + # Now test that the object is valid when changing the scoped attribute + # TODO: There is a chance that we could change the scoped field + # to a value that's already taken. An alternative implementation + # could actually find all values for scope and create a unique + # one. + if !scope.blank? + scope.each do |s| + # Assume the scope is a foreign key if the field is nil + object.send(:"#{s}=", existing.send(s).nil? ? 1 : existing.send(s).next) + end - object.errors.clear - object.valid? - scope.each do |s| - assert_does_not_contain(object.errors.on(attribute), message, - "after :#{s} set to #{object.send(s.to_sym)}") + object.errors.clear + object.valid? + scope.each do |s| + assert_does_not_contain(object.errors.on(attribute), message, + "after :#{s} set to #{object.send(s.to_sym)}") + end end end end end - end - # Ensures that the attribute cannot be set on mass update. - # Requires an existing record. - # - # should_protect_attributes :password, :admin_flag - # - def should_protect_attributes(*attributes) - get_options!(attributes) - klass = model_class + # Ensures that the attribute cannot be set on mass update. + # Requires an existing record. + # + # should_protect_attributes :password, :admin_flag + # + def should_protect_attributes(*attributes) + get_options!(attributes) + klass = model_class - attributes.each do |attribute| - attribute = attribute.to_sym - should "protect #{attribute} from mass updates" do - protected = klass.protected_attributes || [] - accessible = klass.accessible_attributes || [] + attributes.each do |attribute| + attribute = attribute.to_sym + should "protect #{attribute} from mass updates" do + protected = klass.protected_attributes || [] + accessible = klass.accessible_attributes || [] - assert protected.include?(attribute.to_s) || !accessible.include?(attribute.to_s), - (accessible.empty? ? - "#{klass} is protecting #{protected.to_a.to_sentence}, but not #{attribute}." : - "#{klass} has made #{attribute} accessible") + assert protected.include?(attribute.to_s) || !accessible.include?(attribute.to_s), + (accessible.empty? ? + "#{klass} is protecting #{protected.to_a.to_sentence}, but not #{attribute}." : + "#{klass} has made #{attribute} accessible") + end + end + 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" + # + def should_not_allow_values_for(attribute, *bad_values) + message = get_options!(bad_values, :message) + message ||= /invalid/ + klass = model_class + bad_values.each do |v| + should "not allow #{attribute} to be set to #{v.inspect}" 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), message, "when set to \"#{v}\"") + end + end + end + + # 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" + # + def should_allow_values_for(attribute, *good_values) + get_options!(good_values) + klass = model_class + good_values.each do |v| + should "allow #{attribute} to be set to #{v.inspect}" do + assert object = klass.find(:first), "Can't find first #{klass}" + object.send("#{attribute}=", v) + object.save + assert_nil object.errors.on(attribute) + end end end - 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" - # - def should_not_allow_values_for(attribute, *bad_values) - message = get_options!(bad_values, :message) - message ||= /invalid/ - klass = model_class - bad_values.each do |v| - should "not allow #{attribute} to be set to #{v.inspect}" do + # 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) + # + def should_ensure_length_in_range(attribute, range, opts = {}) + short_message, long_message = get_options!([opts], :short_message, :long_message) + short_message ||= /short/ + long_message ||= /long/ + + klass = model_class + min_length = range.first + max_length = range.last + same_length = (min_length == max_length) + + if min_length > 0 + should "not allow #{attribute} to be less than #{min_length} chars long" do + min_value = "x" * (min_length - 1) + 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), short_message, "when set to \"#{min_value}\"") + end + end + + if min_length > 0 + should "allow #{attribute} to be exactly #{min_length} chars long" do + min_value = "x" * min_length + assert object = klass.find(:first), "Can't find first #{klass}" + object.send("#{attribute}=", min_value) + object.save + assert_does_not_contain(object.errors.on(attribute), short_message, "when set to \"#{min_value}\"") + end + end + + should "not allow #{attribute} to be more than #{max_length} chars long" do + max_value = "x" * (max_length + 1) + 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), long_message, "when set to \"#{max_value}\"") + end + + unless same_length + should "allow #{attribute} to be exactly #{max_length} chars long" do + max_value = "x" * max_length + assert object = klass.find(:first), "Can't find first #{klass}" + object.send("#{attribute}=", max_value) + object.save + assert_does_not_contain(object.errors.on(attribute), long_message, "when set to \"#{max_value}\"") + end + end + 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 + # + def should_ensure_length_at_least(attribute, min_length, opts = {}) + short_message = get_options!([opts], :short_message) + short_message ||= /short/ + + klass = model_class + + if min_length > 0 + min_value = "x" * (min_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), short_message, "when set to \"#{min_value}\"") + end + end + should "allow #{attribute} to be at least #{min_length} chars long" do + valid_value = "x" * (min_length) + assert object = klass.find(:first), "Can't find first #{klass}" + object.send("#{attribute}=", valid_value) + assert object.save, "Could not save #{klass} with #{attribute} set to \"#{valid_value}\"" + end + 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) + # + def should_ensure_value_in_range(attribute, range, opts = {}) + low_message, high_message = get_options!([opts], :low_message, :high_message) + low_message ||= /included/ + 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), message, "when set to \"#{v}\"") + assert_contains(object.errors.on(attribute), low_message, "when set to \"#{v}\"") end - end - end - # 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" - # - def should_allow_values_for(attribute, *good_values) - get_options!(good_values) - klass = model_class - good_values.each do |v| - should "allow #{attribute} to be set to #{v.inspect}" do + should "allow #{attribute} to be #{min}" do + v = min assert object = klass.find(:first), "Can't find first #{klass}" object.send("#{attribute}=", v) object.save - assert_nil object.errors.on(attribute) + assert_does_not_contain(object.errors.on(attribute), low_message, "when set to \"#{v}\"") end - end - 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) - # - def should_ensure_length_in_range(attribute, range, opts = {}) - short_message, long_message = get_options!([opts], :short_message, :long_message) - short_message ||= /short/ - long_message ||= /long/ - - klass = model_class - min_length = range.first - max_length = range.last - same_length = (min_length == max_length) - - if min_length > 0 - should "not allow #{attribute} to be less than #{min_length} chars long" do - min_value = "x" * (min_length - 1) + 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}=", 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), short_message, "when set to \"#{min_value}\"") + 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), high_message, "when set to \"#{v}\"") end - end - if min_length > 0 - should "allow #{attribute} to be exactly #{min_length} chars long" do - min_value = "x" * min_length + should "allow #{attribute} to be #{max}" do + v = max assert object = klass.find(:first), "Can't find first #{klass}" - object.send("#{attribute}=", min_value) + object.send("#{attribute}=", v) object.save - assert_does_not_contain(object.errors.on(attribute), short_message, "when set to \"#{min_value}\"") + assert_does_not_contain(object.errors.on(attribute), high_message, "when set to \"#{v}\"") end - end - - should "not allow #{attribute} to be more than #{max_length} chars long" do - max_value = "x" * (max_length + 1) - 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), long_message, "when set to \"#{max_value}\"") - end - - unless same_length - should "allow #{attribute} to be exactly #{max_length} chars long" do - max_value = "x" * max_length - assert object = klass.find(:first), "Can't find first #{klass}" - object.send("#{attribute}=", max_value) - object.save - assert_does_not_contain(object.errors.on(attribute), long_message, "when set to \"#{max_value}\"") - end - end - 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 - # - def should_ensure_length_at_least(attribute, min_length, opts = {}) - short_message = get_options!([opts], :short_message) - short_message ||= /short/ - - klass = model_class - - if min_length > 0 - min_value = "x" * (min_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), short_message, "when set to \"#{min_value}\"") - end - end - should "allow #{attribute} to be at least #{min_length} chars long" do - valid_value = "x" * (min_length) - assert object = klass.find(:first), "Can't find first #{klass}" - object.send("#{attribute}=", valid_value) - assert object.save, "Could not save #{klass} with #{attribute} set to \"#{valid_value}\"" - end - 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) - # - def should_ensure_value_in_range(attribute, range, opts = {}) - low_message, high_message = get_options!([opts], :low_message, :high_message) - low_message ||= /included/ - high_message ||= /included/ + end - 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), low_message, "when set to \"#{v}\"") - end - - should "allow #{attribute} to be #{min}" do - v = min - assert object = klass.find(:first), "Can't find first #{klass}" - object.send("#{attribute}=", v) - object.save - assert_does_not_contain(object.errors.on(attribute), 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), high_message, "when set to \"#{v}\"") - end - - should "allow #{attribute} to be #{max}" do - v = max - assert object = klass.find(:first), "Can't find first #{klass}" - object.send("#{attribute}=", v) - object.save - assert_does_not_contain(object.errors.on(attribute), high_message, "when set to \"#{v}\"") - end - 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 - # - def should_only_allow_numeric_values_for(*attributes) - message = get_options!(attributes, :message) - 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), message) + # 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 + # + def should_only_allow_numeric_values_for(*attributes) + message = get_options!(attributes, :message) + 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), message) + end end end - 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| - name = "have many #{association}" - name += " through #{through}" if through - name += " dependent => #{dependent}" if dependent - should name do - reflection = klass.reflect_on_association(association) - assert reflection, "#{klass.name} does not have any relationship to #{association}" - assert_equal :has_many, reflection.macro + # 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| + name = "have many #{association}" + name += " through #{through}" if through + name += " dependent => #{dependent}" if dependent + should name do + reflection = klass.reflect_on_association(association) + assert reflection, "#{klass.name} does not have any relationship to #{association}" + assert_equal :has_many, reflection.macro - associated_klass = (reflection.options[:class_name] || association.to_s.classify).constantize + associated_klass = (reflection.options[:class_name] || association.to_s.classify).constantize - if through - through_reflection = klass.reflect_on_association(through) - assert through_reflection, "#{klass.name} does not have any relationship to #{through}" - assert_equal(through, reflection.options[:through]) + if through + through_reflection = klass.reflect_on_association(through) + assert through_reflection, "#{klass.name} does not have any relationship to #{through}" + assert_equal(through, reflection.options[:through]) + end + + if dependent + assert_equal dependent.to_s, + reflection.options[:dependent].to_s, + "#{associated_klass.name} should have #{dependent} dependency" + end + + # Check for the existence of the foreign key on the other table + unless reflection.options[:through] + if reflection.options[:foreign_key] + fk = reflection.options[:foreign_key] + elsif reflection.options[:as] + fk = reflection.options[:as].to_s.foreign_key + else + fk = reflection.primary_key_name + end + + assert associated_klass.column_names.include?(fk.to_s), + "#{associated_klass.name} does not have a #{fk} foreign key." + end 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. + # + # Example: + # should_have_one :god # unless hindu + # + def should_have_one(*associations) + get_options!(associations) + klass = model_class + associations.each do |association| + should "have one #{association}" do + reflection = klass.reflect_on_association(association) + assert reflection, "#{klass.name} does not have any relationship to #{association}" + assert_equal :has_one, reflection.macro + + associated_klass = (reflection.options[:class_name] || association.to_s.camelize).constantize - if dependent - assert_equal dependent.to_s, - reflection.options[:dependent].to_s, - "#{associated_klass.name} should have #{dependent} dependency" - end - - # Check for the existence of the foreign key on the other table - unless reflection.options[:through] if reflection.options[:foreign_key] fk = reflection.options[:foreign_key] elsif reflection.options[:as] fk = reflection.options[:as].to_s.foreign_key + fk_type = fk.gsub(/_id$/, '_type') + assert associated_klass.column_names.include?(fk_type), + "#{associated_klass.name} does not have a #{fk_type} column." else - fk = reflection.primary_key_name + fk = klass.name.foreign_key end - - assert associated_klass.column_names.include?(fk.to_s), - "#{associated_klass.name} does not have a #{fk} foreign key." + assert associated_klass.column_names.include?(fk.to_s), + "#{associated_klass.name} does not have a #{fk} foreign key." end 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 - # 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 - # - def should_have_one(*associations) - get_options!(associations) - klass = model_class - associations.each do |association| - should "have one #{association}" do - reflection = klass.reflect_on_association(association) - assert reflection, "#{klass.name} does not have any relationship to #{association}" - assert_equal :has_one, reflection.macro - - associated_klass = (reflection.options[:class_name] || association.to_s.camelize).constantize - - if reflection.options[:foreign_key] - fk = reflection.options[:foreign_key] - elsif reflection.options[:as] - fk = reflection.options[:as].to_s.foreign_key - fk_type = fk.gsub(/_id$/, '_type') - assert associated_klass.column_names.include?(fk_type), - "#{associated_klass.name} does not have a #{fk_type} column." - else - fk = klass.name.foreign_key - end - assert associated_klass.column_names.include?(fk.to_s), - "#{associated_klass.name} does not have a #{fk} foreign key." - 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| - should "should have and belong to many #{association}" do - reflection = klass.reflect_on_association(association) - assert reflection, "#{klass.name} does not have any relationship to #{association}" - assert_equal :has_and_belongs_to_many, reflection.macro - table = reflection.options[:join_table] - assert ::ActiveRecord::Base.connection.tables.include?(table), "table #{table} doesn't exist" - end - end - end - - # Ensure that the belongs_to relationship exists. - # - # should_belong_to :parent - # - def should_belong_to(*associations) - get_options!(associations) - klass = model_class - associations.each do |association| - should "belong_to #{association}" do - reflection = klass.reflect_on_association(association) - assert reflection, "#{klass.name} does not have any relationship to #{association}" - assert_equal :belongs_to, reflection.macro - - unless reflection.options[:polymorphic] - associated_klass = (reflection.options[:class_name] || association.to_s.classify).constantize - fk = reflection.options[:foreign_key] || reflection.primary_key_name - assert klass.column_names.include?(fk.to_s), "#{klass.name} does not have a #{fk} foreign key." + associations.each do |association| + should "should have and belong to many #{association}" do + reflection = klass.reflect_on_association(association) + assert reflection, "#{klass.name} does not have any relationship to #{association}" + assert_equal :has_and_belongs_to_many, reflection.macro + table = reflection.options[:join_table] + assert ::ActiveRecord::Base.connection.tables.include?(table), "table #{table} doesn't exist" end 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}" + + # Ensure that the belongs_to relationship exists. + # + # should_belong_to :parent + # + def should_belong_to(*associations) + get_options!(associations) + klass = model_class + associations.each do |association| + should "belong_to #{association}" do + reflection = klass.reflect_on_association(association) + assert reflection, "#{klass.name} does not have any relationship to #{association}" + assert_equal :belongs_to, reflection.macro + + unless reflection.options[:polymorphic] + associated_klass = (reflection.options[:class_name] || association.to_s.classify).constantize + fk = reflection.options[:foreign_key] || reflection.primary_key_name + assert klass.column_names.include?(fk.to_s), "#{klass.name} does not have a #{fk} foreign key." + end + end 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}" + + # 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 - end - # Ensure that the given columns are defined on the models backing SQL table. - # - # should_have_db_columns :id, :email, :name, :created_at - # - def should_have_db_columns(*columns) - column_type = get_options!(columns, :type) - klass = model_class - columns.each do |name| - test_name = "have column #{name}" - test_name += " of type #{column_type}" if column_type + # 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. + # + # should_have_db_columns :id, :email, :name, :created_at + # + def should_have_db_columns(*columns) + column_type = get_options!(columns, :type) + klass = model_class + columns.each do |name| + test_name = "have column #{name}" + test_name += " of type #{column_type}" if column_type + should test_name do + column = klass.columns.detect {|c| c.name == name.to_s } + assert column, "#{klass.name} does not have column #{name}" + end + end + 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)' + # + def should_have_db_column(name, opts = {}) + klass = model_class + test_name = "have column named :#{name}" + test_name += " with options " + opts.inspect unless opts.empty? should test_name do column = klass.columns.detect {|c| c.name == name.to_s } assert column, "#{klass.name} does not have column #{name}" + opts.each do |k, v| + assert_equal column.instance_variable_get("@#{k}").to_s, v.to_s, ":#{name} column on table for #{klass} does not match option :#{k}" + end end end - 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)' - # - def should_have_db_column(name, opts = {}) - klass = model_class - test_name = "have column named :#{name}" - test_name += " with options " + opts.inspect unless opts.empty? - should test_name do - column = klass.columns.detect {|c| c.name == name.to_s } - assert column, "#{klass.name} does not have column #{name}" - opts.each do |k, v| - assert_equal column.instance_variable_get("@#{k}").to_s, v.to_s, ":#{name} column on table for #{klass} does not match option :#{k}" + # 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 + # + def should_have_indices(*columns) + table = model_class.name.tableize + indices = ::ActiveRecord::Base.connection.indexes(table).map(&:columns) + + columns.each do |column| + should "have index on #{table} for #{column.inspect}" do + columns = [column].flatten.map(&:to_s) + assert_contains(indices, columns) + end end end - end - # 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 - # - def should_have_indices(*columns) - table = model_class.name.tableize - indices = ::ActiveRecord::Base.connection.indexes(table).map(&:columns) - - columns.each do |column| - should "have index on #{table} for #{column.inspect}" do - columns = [column].flatten.map(&:to_s) - assert_contains(indices, columns) - end - end - end - - alias_method :should_have_index, :should_have_indices + alias_method :should_have_index, :should_have_indices + + # 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 + # + def should_require_acceptance_of(*attributes) + message = get_options!(attributes, :message) + message ||= /must be accepted/ + klass = model_class - # 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 - # - def should_require_acceptance_of(*attributes) - message = get_options!(attributes, :message) - message ||= /must be accepted/ - klass = model_class - - attributes.each do |attribute| - should "require #{attribute} to be accepted" do - object = klass.new - object.send("#{attribute}=", false) + attributes.each do |attribute| + should "require #{attribute} to be accepted" do + object = klass.new + object.send("#{attribute}=", false) - assert !object.valid?, "#{klass.name} does not require acceptance of #{attribute}." - assert object.errors.on(attribute), "#{klass.name} does not require acceptance of #{attribute}." - assert_contains(object.errors.on(attribute), message) + assert !object.valid?, "#{klass.name} does not require acceptance of #{attribute}." + assert object.errors.on(attribute), "#{klass.name} does not require acceptance of #{attribute}." + assert_contains(object.errors.on(attribute), message) + end end end + + private + + include ThoughtBot::Shoulda::Private end - - private - - include Shoulda::Private end end diff --git a/lib/shoulda/color.rb b/lib/shoulda/color.rb index 170ae2c1..1ccfad2b 100644 --- a/lib/shoulda/color.rb +++ b/lib/shoulda/color.rb @@ -1,8 +1,6 @@ require 'test/unit/ui/console/testrunner' -# Completely stolen from redgreen gem (thanks Pat Eyler and Chris Wanstrath). -# -# NOTE: Is this now in autotest? Can I remove this, then? +# Completely stolen from redgreen gem # # Adds colored output to your tests. Specify color: true in # your ~/.shoulda.conf file to enable. @@ -11,7 +9,7 @@ require 'test/unit/ui/console/testrunner' # every rake task, as though there was another (empty) set of tests. # A fix would be most welcome. # -module Shoulda::Color +module ThoughtBot::Shoulda::Color COLORS = { :clear => 0, :red => 31, :green => 32, :yellow => 33 } # :nodoc: def self.method_missing(color_name, *args) # :nodoc: color(color_name) + args.first + color(:clear) @@ -27,7 +25,7 @@ module Test # :nodoc: alias :old_to_s :to_s def to_s if old_to_s =~ /\d+ tests, \d+ assertions, (\d+) failures, (\d+) errors/ - Shoulda::Color.send($1.to_i != 0 || $2.to_i != 0 ? :red : :green, $&) + ThoughtBot::Shoulda::Color.send($1.to_i != 0 || $2.to_i != 0 ? :red : :green, $&) end end end @@ -45,16 +43,16 @@ module Test # :nodoc: class Failure # :nodoc: alias :old_long_display :long_display def long_display - # old_long_display.sub('Failure', Shoulda::Color.red('Failure')) - Shoulda::Color.red(old_long_display) + # old_long_display.sub('Failure', ThoughtBot::Shoulda::Color.red('Failure')) + ThoughtBot::Shoulda::Color.red(old_long_display) end end class Error # :nodoc: alias :old_long_display :long_display def long_display - # old_long_display.sub('Error', Shoulda::Color.yellow('Error')) - Shoulda::Color.yellow(old_long_display) + # old_long_display.sub('Error', ThoughtBot::Shoulda::Color.yellow('Error')) + ThoughtBot::Shoulda::Color.yellow(old_long_display) end end @@ -64,9 +62,9 @@ module Test # :nodoc: def output_single(something, level=NORMAL) return unless (output?(level)) something = case something - when '.' then Shoulda::Color.green('.') - when 'F' then Shoulda::Color.red("F") - when 'E' then Shoulda::Color.yellow("E") + when '.' then ThoughtBot::Shoulda::Color.green('.') + when 'F' then ThoughtBot::Shoulda::Color.red("F") + when 'E' then ThoughtBot::Shoulda::Color.yellow("E") else something end @io.write(something) diff --git a/lib/shoulda/context.rb b/lib/shoulda/context.rb deleted file mode 100644 index 3520ae5c..00000000 --- a/lib/shoulda/context.rb +++ /dev/null @@ -1,140 +0,0 @@ -require File.join(File.dirname(__FILE__), 'extensions', 'proc') - -module Shoulda - class Context - attr_accessor :name # my name - attr_accessor :parent # may be another context, or the original test::unit class. - attr_accessor :subcontexts # array of contexts nested under myself - attr_accessor :setup_block # block given via a setup method - attr_accessor :teardown_block # block given via a teardown method - attr_accessor :shoulds # array of hashes representing the should statements - attr_accessor :should_eventuallys # array of hashes representing the should eventually statements - - def initialize(name, parent, &blk) - Shoulda.current_context = self - self.name = name - self.parent = parent - self.setup_block = nil - self.teardown_block = nil - self.shoulds = [] - self.should_eventuallys = [] - self.subcontexts = [] - - blk.bind(self).call - Shoulda.current_context = nil - end - - # Creates a context. See Shoulda#context. - def context(name, &blk) - subcontexts << Context.new(name, self, &blk) - Shoulda.current_context = self - end - - # Creates a should statement. See Shoulda#should. - def should(name, &blk) - self.shoulds << { :name => name, :block => blk } - end - - # Creates a should_eventually statement. See Shoulda#should_eventually. - def should_eventually(name, &blk) - self.should_eventuallys << { :name => name, :block => blk } - end - - # Any code in a setup block will be run before the should statements in a - # context. Nested contexts will have their setup blocks run in order. - def setup(&blk) - self.setup_block = blk - end - - # Any code in a teardown block will be run after the should statements in a - # context. Nested contexts will have their teardown blocks run in reverse - # order. - def teardown(&blk) - self.teardown_block = blk - end - - # The full name of this context, including parents. - def full_name - parent_name = parent.full_name if subcontext? - return [parent_name, name].join(" ").strip - end - - # Returns true if this context is nested - def subcontext? - parent.is_a?(self.class) # my parent is the same class as myself. - end - - # Returns the root class that decends from Test::Unit. - def test_unit_class - subcontext? ? parent.test_unit_class : parent - end - - - # Creates a single test from a should hash - def create_test_from_should_hash(should) - test_name = ["test:", full_name, "should", "#{should[:name]}. "].flatten.join(' ').to_sym - - if test_unit_class.instance_methods.include?(test_name.to_s) - warn " * WARNING: '#{test_name}' is already defined" - end - - context = self - test_unit_class.send(:define_method, test_name) do |*args| - begin - context.run_all_setup_blocks(self) - should[:block].bind(self).call - ensure - context.run_all_teardown_blocks(self) - end - end - end - - # Runs all the setup blocks in order - def run_all_setup_blocks(binding) - self.parent.run_all_setup_blocks(binding) if subcontext? - setup_block.bind(binding).call if setup_block - end - - # Runs all the teardown blocks in reverse order - def run_all_teardown_blocks(binding) - teardown_block.bind(binding).call if teardown_block - self.parent.run_all_teardown_blocks(binding) if subcontext? - end - - # Prints the should_eventually names to stdout - def print_should_eventuallys - should_eventuallys.each do |should| - test_name = [full_name, "should", "#{should[:name]}. "].flatten.join(' ') - puts " * DEFERRED: " + test_name - end - subcontexts.each { |context| context.print_should_eventuallys } - end - - # Triggers the test method creation process, and prints the unimplemented tests. - def build - shoulds.each do |should| - create_test_from_should_hash(should) - end - - subcontexts.each { |context| context.build } - - print_should_eventuallys - end - - # This delegates all method calls inside a context to the surrounding - # Test::Unit class. This allows us to call Test::Unit macros inside a - # context. - def method_missing(method, *args, &blk) - test_unit_class.send(method, *args, &blk) - end - end -end - -module Test # :nodoc: all - module Unit - class TestCase - extend Shoulda - end - end -end - diff --git a/lib/shoulda/controller_tests/controller_tests.rb b/lib/shoulda/controller_tests/controller_tests.rb index 7478f026..f0059c0f 100644 --- a/lib/shoulda/controller_tests/controller_tests.rb +++ b/lib/shoulda/controller_tests/controller_tests.rb @@ -1,472 +1,467 @@ -module Shoulda - module Controller - def self.included(other) # :nodoc: - other.class_eval do - extend Shoulda::Controller::ClassMethods - include Shoulda::Controller::InstanceMethods - Shoulda::Controller::ClassMethods::VALID_FORMATS.each do |format| - include "Shoulda::Controller::#{format.to_s.upcase}".constantize +module ThoughtBot # :nodoc: + module Shoulda # :nodoc: + module Controller + def self.included(other) # :nodoc: + other.class_eval do + extend ThoughtBot::Shoulda::Controller::ClassMethods + include ThoughtBot::Shoulda::Controller::InstanceMethods + ThoughtBot::Shoulda::Controller::ClassMethods::VALID_FORMATS.each do |format| + include "ThoughtBot::Shoulda::Controller::#{format.to_s.upcase}".constantize + end end end - end - - # = Macro test helpers for your controllers - # - # By using the macro helpers you can quickly and easily create concise and easy to read test suites. - # - # This code segment: - # context "on GET to :show for first record" do - # setup do - # get :show, :id => 1 - # end - # - # should_assign_to :user - # should_respond_with :success - # should_render_template :show - # should_not_set_the_flash - # - # should "do something else really cool" do - # assert_equal 1, assigns(:user).id - # end - # end - # - # Would produce 5 tests for the +show+ action - # - # Furthermore, the should_be_restful helper will create an entire set of tests which will verify that your - # controller responds restfully to a variety of requested formats. - module ClassMethods - # Formats tested by #should_be_restful. Defaults to [:html, :xml] - VALID_FORMATS = Dir.glob(File.join(File.dirname(__FILE__), 'formats', '*.rb')).map { |f| File.basename(f, '.rb') }.map(&:to_sym) # :doc: - VALID_FORMATS.each {|f| require "shoulda/controller_tests/formats/#{f}.rb"} - - # Actions tested by #should_be_restful - VALID_ACTIONS = [:index, :show, :new, :edit, :create, :update, :destroy] # :doc: - - # A ResourceOptions object is passed into should_be_restful in order to configure the tests for your controller. + + # = Macro test helpers for your controllers + # + # By using the macro helpers you can quickly and easily create concise and easy to read test suites. # - # Example: - # class UsersControllerTest < Test::Unit::TestCase - # load_all_fixtures - # - # def setup - # ...normal setup code... - # @user = User.find(:first) + # This code segment: + # context "on GET to :show for first record" do + # setup do + # get :show, :id => 1 # end # - # should_be_restful do |resource| - # resource.identifier = :id - # resource.klass = User - # resource.object = :user - # resource.parent = [] - # resource.actions = [:index, :show, :new, :edit, :update, :create, :destroy] - # resource.formats = [:html, :xml] - # - # resource.create.params = { :name => "bob", :email => 'bob@bob.com', :age => 13} - # resource.update.params = { :name => "sue" } - # - # resource.create.redirect = "user_url(@user)" - # resource.update.redirect = "user_url(@user)" - # resource.destroy.redirect = "users_url" - # - # resource.create.flash = /created/i - # resource.update.flash = /updated/i - # resource.destroy.flash = /removed/i + # should_assign_to :user + # should_respond_with :success + # should_render_template :show + # should_not_set_the_flash + # + # should "do something else really cool" do + # assert_equal 1, assigns(:user).id # end # end # - # Whenever possible, the resource attributes will be set to sensible defaults. + # Would produce 5 tests for the +show+ action # - class ResourceOptions - # Configuration options for the create, update, destroy actions under should_be_restful - class ActionOptions - # String evaled to get the target of the redirection. - # All of the instance variables set by the controller will be available to the - # evaled code. - # - # Example: - # resource.create.redirect = "user_url(@user.company, @user)" - # - # Defaults to a generated url based on the name of the controller, the action, and the resource.parents list. - attr_accessor :redirect + # Furthermore, the should_be_restful helper will create an entire set of tests which will verify that your + # controller responds restfully to a variety of requested formats. + module ClassMethods + # Formats tested by #should_be_restful. Defaults to [:html, :xml] + VALID_FORMATS = Dir.glob(File.join(File.dirname(__FILE__), 'formats', '*.rb')).map { |f| File.basename(f, '.rb') }.map(&:to_sym) # :doc: + VALID_FORMATS.each {|f| require "shoulda/controller_tests/formats/#{f}.rb"} - # String or Regexp describing a value expected in the flash. Will match against any flash key. - # - # Defaults: - # destroy:: /removed/ - # create:: /created/ - # update:: /updated/ - attr_accessor :flash - - # Hash describing the params that should be sent in with this action. - attr_accessor :params - end + # Actions tested by #should_be_restful + VALID_ACTIONS = [:index, :show, :new, :edit, :create, :update, :destroy] # :doc: - # Configuration options for the denied actions under should_be_restful - # + # A ResourceOptions object is passed into should_be_restful in order to configure the tests for your controller. + # # Example: - # context "The public" do - # setup do - # @request.session[:logged_in] = false + # class UsersControllerTest < Test::Unit::TestCase + # load_all_fixtures + # + # def setup + # ...normal setup code... + # @user = User.find(:first) # end # # should_be_restful do |resource| - # resource.parent = :user + # resource.identifier = :id + # resource.klass = User + # resource.object = :user + # resource.parent = [] + # resource.actions = [:index, :show, :new, :edit, :update, :create, :destroy] + # resource.formats = [:html, :xml] # - # resource.denied.actions = [:index, :show, :edit, :new, :create, :update, :destroy] - # resource.denied.flash = /get outta here/i - # resource.denied.redirect = 'new_session_url' - # end + # resource.create.params = { :name => "bob", :email => 'bob@bob.com', :age => 13} + # resource.update.params = { :name => "sue" } + # + # resource.create.redirect = "user_url(@user)" + # resource.update.redirect = "user_url(@user)" + # resource.destroy.redirect = "users_url" + # + # resource.create.flash = /created/i + # resource.update.flash = /updated/i + # resource.destroy.flash = /removed/i + # end # end # - class DeniedOptions - # String evaled to get the target of the redirection. - # All of the instance variables set by the controller will be available to the - # evaled code. + # Whenever possible, the resource attributes will be set to sensible defaults. + # + class ResourceOptions + # Configuration options for the create, update, destroy actions under should_be_restful + class ActionOptions + # String evaled to get the target of the redirection. + # All of the instance variables set by the controller will be available to the + # evaled code. + # + # Example: + # resource.create.redirect = "user_url(@user.company, @user)" + # + # Defaults to a generated url based on the name of the controller, the action, and the resource.parents list. + attr_accessor :redirect + + # String or Regexp describing a value expected in the flash. Will match against any flash key. + # + # Defaults: + # destroy:: /removed/ + # create:: /created/ + # update:: /updated/ + attr_accessor :flash + + # Hash describing the params that should be sent in with this action. + attr_accessor :params + end + + # Configuration options for the denied actions under should_be_restful # # Example: - # resource.create.redirect = "user_url(@user.company, @user)" - attr_accessor :redirect + # context "The public" do + # setup do + # @request.session[:logged_in] = false + # end + # + # should_be_restful do |resource| + # resource.parent = :user + # + # resource.denied.actions = [:index, :show, :edit, :new, :create, :update, :destroy] + # resource.denied.flash = /get outta here/i + # resource.denied.redirect = 'new_session_url' + # end + # end + # + class DeniedOptions + # String evaled to get the target of the redirection. + # All of the instance variables set by the controller will be available to the + # evaled code. + # + # Example: + # resource.create.redirect = "user_url(@user.company, @user)" + attr_accessor :redirect - # String or Regexp describing a value expected in the flash. Will match against any flash key. + # String or Regexp describing a value expected in the flash. Will match against any flash key. + # + # Example: + # resource.create.flash = /created/ + attr_accessor :flash + + # Actions that should be denied (only used by resource.denied). Note that these actions will + # only be tested if they are also listed in +resource.actions+ + # The special value of :all will deny all of the REST actions. + attr_accessor :actions + end + + # Name of key in params that references the primary key. + # Will almost always be :id (default), unless you are using a plugin or have patched rails. + attr_accessor :identifier + + # Name of the ActiveRecord class this resource is responsible for. Automatically determined from + # test class if not explicitly set. UserTest => "User" + attr_accessor :klass + + # Name of the instantiated ActiveRecord object that should be used by some of the tests. + # Defaults to the underscored name of the AR class. CompanyManager => :company_manager + attr_accessor :object + + # Name of the parent AR objects. Can be set as parent= or parents=, and can take either + # the name of the parent resource (if there's only one), or an array of names (if there's + # more than one). # # Example: - # resource.create.flash = /created/ - attr_accessor :flash - - # Actions that should be denied (only used by resource.denied). Note that these actions will - # only be tested if they are also listed in +resource.actions+ - # The special value of :all will deny all of the REST actions. + # # in the routes... + # map.resources :companies do + # map.resources :people do + # map.resources :limbs + # end + # end + # + # # in the tests... + # class PeopleControllerTest < Test::Unit::TestCase + # should_be_restful do |resource| + # resource.parent = :companies + # end + # end + # + # class LimbsControllerTest < Test::Unit::TestCase + # should_be_restful do |resource| + # resource.parents = [:companies, :people] + # end + # end + attr_accessor :parent + alias parents parent + alias parents= parent= + + # Actions that should be tested. Must be a subset of VALID_ACTIONS (default). + # Tests for each actionw will only be generated if the action is listed here. + # The special value of :all will test all of the REST actions. + # + # Example (for a read-only controller): + # resource.actions = [:show, :index] attr_accessor :actions + + # Formats that should be tested. Must be a subset of VALID_FORMATS (default). + # Each action will be tested against the formats listed here. The special value + # of :all will test all of the supported formats. + # + # Example: + # resource.actions = [:html, :xml] + attr_accessor :formats + + # ActionOptions object specifying options for the create action. + attr_accessor :create + + # ActionOptions object specifying options for the update action. + attr_accessor :update + + # ActionOptions object specifying options for the desrtoy action. + attr_accessor :destroy + + # DeniedOptions object specifying which actions should return deny a request, and what should happen in that case. + attr_accessor :denied + + def initialize # :nodoc: + @create = ActionOptions.new + @update = ActionOptions.new + @destroy = ActionOptions.new + @denied = DeniedOptions.new + + @create.flash ||= /created/i + @update.flash ||= /updated/i + @destroy.flash ||= /removed/i + @denied.flash ||= /denied/i + + @create.params ||= {} + @update.params ||= {} + + @actions = VALID_ACTIONS + @formats = VALID_FORMATS + @denied.actions = [] + end + + def normalize!(target) # :nodoc: + @denied.actions = VALID_ACTIONS if @denied.actions == :all + @actions = VALID_ACTIONS if @actions == :all + @formats = VALID_FORMATS if @formats == :all + + @denied.actions = @denied.actions.map(&:to_sym) + @actions = @actions.map(&:to_sym) + @formats = @formats.map(&:to_sym) + + ensure_valid_members(@actions, VALID_ACTIONS, 'actions') + ensure_valid_members(@denied.actions, VALID_ACTIONS, 'denied.actions') + ensure_valid_members(@formats, VALID_FORMATS, 'formats') + + @identifier ||= :id + @klass ||= target.name.gsub(/ControllerTest$/, '').singularize.constantize + @object ||= @klass.name.tableize.singularize + @parent ||= [] + @parent = [@parent] unless @parent.is_a? Array + + collection_helper = [@parent, @object.to_s.pluralize, 'url'].flatten.join('_') + collection_args = @parent.map {|n| "@#{object}.#{n}"}.join(', ') + @destroy.redirect ||= "#{collection_helper}(#{collection_args})" + + member_helper = [@parent, @object, 'url'].flatten.join('_') + member_args = [@parent.map {|n| "@#{object}.#{n}"}, "@#{object}"].flatten.join(', ') + @create.redirect ||= "#{member_helper}(#{member_args})" + @update.redirect ||= "#{member_helper}(#{member_args})" + @denied.redirect ||= "new_session_url" + end + + private + + def ensure_valid_members(ary, valid_members, name) # :nodoc: + invalid = ary - valid_members + raise ArgumentError, "Unsupported #{name}: #{invalid.inspect}" unless invalid.empty? + end end - # Name of key in params that references the primary key. - # Will almost always be :id (default), unless you are using a plugin or have patched rails. - attr_accessor :identifier - - # Name of the ActiveRecord class this resource is responsible for. Automatically determined from - # test class if not explicitly set. UserTest => "User" - attr_accessor :klass - - # Name of the instantiated ActiveRecord object that should be used by some of the tests. - # Defaults to the underscored name of the AR class. CompanyManager => :company_manager - attr_accessor :object - - # Name of the parent AR objects. Can be set as parent= or parents=, and can take either - # the name of the parent resource (if there's only one), or an array of names (if there's - # more than one). + # :section: should_be_restful + # Generates a full suite of tests for a restful controller. # - # Example: - # # in the routes... - # map.resources :companies do - # map.resources :people do - # map.resources :limbs - # end + # The following definition will generate tests for the +index+, +show+, +new+, + # +edit+, +create+, +update+ and +destroy+ actions, in both +html+ and +xml+ formats: + # + # should_be_restful do |resource| + # resource.parent = :user + # + # resource.create.params = { :title => "first post", :body => 'blah blah blah'} + # resource.update.params = { :title => "changed" } # end # - # # in the tests... - # class PeopleControllerTest < Test::Unit::TestCase - # should_be_restful do |resource| - # resource.parent = :companies - # end - # end + # This generates about 40 tests, all of the format: + # "on GET to :show should assign @user." + # "on GET to :show should not set the flash." + # "on GET to :show should render 'show' template." + # "on GET to :show should respond with success." + # "on GET to :show as xml should assign @user." + # "on GET to :show as xml should have ContentType set to 'application/xml'." + # "on GET to :show as xml should respond with success." + # "on GET to :show as xml should return as the root element." + # The +resource+ parameter passed into the block is a ResourceOptions object, and + # is used to configure the tests for the details of your resources. # - # class LimbsControllerTest < Test::Unit::TestCase - # should_be_restful do |resource| - # resource.parents = [:companies, :people] - # end - # end - attr_accessor :parent - alias parents parent - alias parents= parent= - - # Actions that should be tested. Must be a subset of VALID_ACTIONS (default). - # Tests for each actionw will only be generated if the action is listed here. - # The special value of :all will test all of the REST actions. - # - # Example (for a read-only controller): - # resource.actions = [:show, :index] - attr_accessor :actions + def should_be_restful(&blk) # :yields: resource + resource = ResourceOptions.new + blk.call(resource) + resource.normalize!(self) - # Formats that should be tested. Must be a subset of VALID_FORMATS (default). - # Each action will be tested against the formats listed here. The special value - # of :all will test all of the supported formats. - # - # Example: - # resource.actions = [:html, :xml] - attr_accessor :formats - - # ActionOptions object specifying options for the create action. - attr_accessor :create - - # ActionOptions object specifying options for the update action. - attr_accessor :update - - # ActionOptions object specifying options for the desrtoy action. - attr_accessor :destroy - - # DeniedOptions object specifying which actions should return deny a request, and what should happen in that case. - attr_accessor :denied - - def initialize # :nodoc: - @create = ActionOptions.new - @update = ActionOptions.new - @destroy = ActionOptions.new - @denied = DeniedOptions.new - - @create.flash ||= /created/i - @update.flash ||= /updated/i - @destroy.flash ||= /removed/i - @denied.flash ||= /denied/i - - @create.params ||= {} - @update.params ||= {} - - @actions = VALID_ACTIONS - @formats = VALID_FORMATS - @denied.actions = [] - end - - def normalize!(target) # :nodoc: - @denied.actions = VALID_ACTIONS if @denied.actions == :all - @actions = VALID_ACTIONS if @actions == :all - @formats = VALID_FORMATS if @formats == :all - - @denied.actions = @denied.actions.map(&:to_sym) - @actions = @actions.map(&:to_sym) - @formats = @formats.map(&:to_sym) - - ensure_valid_members(@actions, VALID_ACTIONS, 'actions') - ensure_valid_members(@denied.actions, VALID_ACTIONS, 'denied.actions') - ensure_valid_members(@formats, VALID_FORMATS, 'formats') - - @identifier ||= :id - @klass ||= target.name.gsub(/ControllerTest$/, '').singularize.constantize - @object ||= @klass.name.tableize.singularize - @parent ||= [] - @parent = [@parent] unless @parent.is_a? Array - - collection_helper = [@parent, @object.to_s.pluralize, 'url'].flatten.join('_') - collection_args = @parent.map {|n| "@#{object}.#{n}"}.join(', ') - @destroy.redirect ||= "#{collection_helper}(#{collection_args})" - - member_helper = [@parent, @object, 'url'].flatten.join('_') - member_args = [@parent.map {|n| "@#{object}.#{n}"}, "@#{object}"].flatten.join(', ') - @create.redirect ||= "#{member_helper}(#{member_args})" - @update.redirect ||= "#{member_helper}(#{member_args})" - @denied.redirect ||= "new_session_url" - end - - private - - def ensure_valid_members(ary, valid_members, name) # :nodoc: - invalid = ary - valid_members - raise ArgumentError, "Unsupported #{name}: #{invalid.inspect}" unless invalid.empty? - end - end - - # :section: should_be_restful - # Generates a full suite of tests for a restful controller. - # - # The following definition will generate tests for the +index+, +show+, +new+, - # +edit+, +create+, +update+ and +destroy+ actions, in both +html+ and +xml+ formats: - # - # should_be_restful do |resource| - # resource.parent = :user - # - # resource.create.params = { :title => "first post", :body => 'blah blah blah'} - # resource.update.params = { :title => "changed" } - # end - # - # This generates about 40 tests, all of the format: - # "on GET to :show should assign @user." - # "on GET to :show should not set the flash." - # "on GET to :show should render 'show' template." - # "on GET to :show should respond with success." - # "on GET to :show as xml should assign @user." - # "on GET to :show as xml should have ContentType set to 'application/xml'." - # "on GET to :show as xml should respond with success." - # "on GET to :show as xml should return as the root element." - # The +resource+ parameter passed into the block is a ResourceOptions object, and - # is used to configure the tests for the details of your resources. - # - def should_be_restful(&blk) # :yields: resource - resource = ResourceOptions.new - blk.call(resource) - resource.normalize!(self) - - resource.formats.each do |format| - resource.actions.each do |action| - if self.respond_to? :"make_#{action}_#{format}_tests" - self.send(:"make_#{action}_#{format}_tests", resource) - else - should "test #{action} #{format}" do - flunk "Test for #{action} as #{format} not implemented" + resource.formats.each do |format| + resource.actions.each do |action| + if self.respond_to? :"make_#{action}_#{format}_tests" + self.send(:"make_#{action}_#{format}_tests", resource) + else + should "test #{action} #{format}" do + flunk "Test for #{action} as #{format} not implemented" + end end end end end - end - # :section: Test macros - - # Macro that creates a test asserting that the flash contains the given value. - # val can be a String, a Regex, or nil (indicating that the flash should not be set) - # - # Example: - # - # should_set_the_flash_to "Thank you for placing this order." - # should_set_the_flash_to /created/i - # should_set_the_flash_to nil - def should_set_the_flash_to(val) - if val - should "have #{val.inspect} in the flash" do - assert_contains flash.values, val, ", Flash: #{flash.inspect}" + # :section: Test macros + + # Macro that creates a test asserting that the flash contains the given value. + # val can be a String, a Regex, or nil (indicating that the flash should not be set) + # + # Example: + # + # should_set_the_flash_to "Thank you for placing this order." + # should_set_the_flash_to /created/i + # should_set_the_flash_to nil + def should_set_the_flash_to(val) + if val + should "have #{val.inspect} in the flash" do + assert_contains flash.values, val, ", Flash: #{flash.inspect}" + end + else + should "not set the flash" do + assert_equal({}, flash, "Flash was set to:\n#{flash.inspect}") + end end - else - should "not set the flash" do - assert_equal({}, flash, "Flash was set to:\n#{flash.inspect}") + end + + # Macro that creates a test asserting that the flash is empty. Same as + # @should_set_the_flash_to nil@ + def should_not_set_the_flash + should_set_the_flash_to nil + end + + # Macro that creates a test asserting that the controller assigned to @name + # + # Example: + # + # should_assign_to :user + def should_assign_to(name) + should "assign @#{name}" do + assert assigns(name.to_sym), "The action isn't assigning to @#{name}" + end + end + + # Macro that creates a test asserting that the controller did not assign to @name + # + # Example: + # + # should_not_assign_to :user + def should_not_assign_to(name) + should "not assign to @#{name}" do + assert !assigns(name.to_sym), "@#{name} was visible" + end + end + + # Macro that creates a test asserting that the controller responded with a 'response' status code. + # Example: + # + # should_respond_with :success + def should_respond_with(response) + should "respond with #{response}" do + assert_response response + end + end + + # Macro that creates a test asserting that the controller rendered the given template. + # Example: + # + # should_render_template :new + def should_render_template(template) + should "render template #{template.inspect}" do + assert_template template.to_s + end + end + + # Macro that creates a test asserting that the controller returned a redirect to the given path. + # The given string is evaled to produce the resulting redirect path. All of the instance variables + # set by the controller are available to the evaled string. + # Example: + # + # should_redirect_to '"/"' + # should_redirect_to "users_url(@user)" + def should_redirect_to(url) + should "redirect to #{url.inspect}" do + instantiate_variables_from_assigns do + assert_redirected_to eval(url, self.send(:binding), __FILE__, __LINE__) + end + end + end + + # Macro that creates a test asserting that the rendered view contains a element. + def should_render_a_form + should "display a form" do + assert_select "form", true, "The template doesn't contain a element" end end end - - # Macro that creates a test asserting that the flash is empty. Same as - # @should_set_the_flash_to nil@ - def should_not_set_the_flash - should_set_the_flash_to nil - end - - # Macro that creates a test asserting that the controller assigned to @name - # - # Example: - # - # should_assign_to :user - def should_assign_to(name) - should "assign @#{name}" do - assert assigns(name.to_sym), "The action isn't assigning to @#{name}" - end - end - # Macro that creates a test asserting that the controller did not assign to @name - # - # Example: - # - # should_not_assign_to :user - def should_not_assign_to(name) - should "not assign to @#{name}" do - assert !assigns(name.to_sym), "@#{name} was visible" - end - end - - # Macro that creates a test asserting that the controller responded with a 'response' status code. - # Example: - # - # should_respond_with :success - def should_respond_with(response) - should "respond with #{response}" do - assert_response response - end - end - - # Macro that creates a test asserting that the controller rendered the given template. - # Example: - # - # should_render_template :new - def should_render_template(template) - should "render template #{template.inspect}" do - assert_template template.to_s - end - end - - # Macro that creates a test asserting that the controller returned a redirect to the given path. - # The given string is evaled to produce the resulting redirect path. All of the instance variables - # set by the controller are available to the evaled string. - # Example: - # - # should_redirect_to '"/"' - # should_redirect_to "users_url(@user)" - def should_redirect_to(url) - should "redirect to #{url.inspect}" do - instantiate_variables_from_assigns do - assert_redirected_to eval(url, self.send(:binding), __FILE__, __LINE__) + module InstanceMethods # :nodoc: + + private # :enddoc: + + SPECIAL_INSTANCE_VARIABLES = %w{ + _cookies + _flash + _headers + _params + _request + _response + _session + action_name + before_filter_chain_aborted + cookies + flash + headers + ignore_missing_templates + logger + params + request + request_origin + response + session + template + template_class + template_root + url + variables_added + }.map(&:to_s) + + def instantiate_variables_from_assigns(*names, &blk) + old = {} + names = (@response.template.assigns.keys - SPECIAL_INSTANCE_VARIABLES) if names.empty? + names.each do |name| + old[name] = instance_variable_get("@#{name}") + instance_variable_set("@#{name}", assigns(name.to_sym)) + end + blk.call + names.each do |name| + instance_variable_set("@#{name}", old[name]) end end - end - - # Macro that creates a test asserting that the rendered view contains a element. - def should_render_a_form - should "display a form" do - assert_select "form", true, "The template doesn't contain a element" + + def get_existing_record(res) # :nodoc: + returning(instance_variable_get("@#{res.object}")) do |record| + assert(record, "This test requires you to set @#{res.object} in your setup block") + end end - end - end - module InstanceMethods # :nodoc: - - private # :enddoc: - - SPECIAL_INSTANCE_VARIABLES = %w{ - _cookies - _flash - _headers - _params - _request - _response - _session - action_name - before_filter_chain_aborted - cookies - flash - headers - ignore_missing_templates - logger - params - request - request_origin - response - session - template - template_class - template_root - url - variables_added - }.map(&:to_s) - - def instantiate_variables_from_assigns(*names, &blk) - old = {} - names = (@response.template.assigns.keys - SPECIAL_INSTANCE_VARIABLES) if names.empty? - names.each do |name| - old[name] = instance_variable_get("@#{name}") - instance_variable_set("@#{name}", assigns(name.to_sym)) - end - blk.call - names.each do |name| - instance_variable_set("@#{name}", old[name]) + def make_parent_params(resource, record = nil, parent_names = nil) # :nodoc: + parent_names ||= resource.parents.reverse + return {} if parent_names == [] # Base case + parent_name = parent_names.shift + parent = record ? record.send(parent_name) : parent_name.to_s.classify.constantize.find(:first) + + { :"#{parent_name}_id" => parent.to_param }.merge(make_parent_params(resource, parent, parent_names)) end + end - - def get_existing_record(res) # :nodoc: - returning(instance_variable_get("@#{res.object}")) do |record| - assert(record, "This test requires you to set @#{res.object} in your setup block") - end - end - - def make_parent_params(resource, record = nil, parent_names = nil) # :nodoc: - parent_names ||= resource.parents.reverse - return {} if parent_names == [] # Base case - parent_name = parent_names.shift - parent = record ? record.send(parent_name) : parent_name.to_s.classify.constantize.find(:first) - - { :"#{parent_name}_id" => parent.to_param }.merge(make_parent_params(resource, parent, parent_names)) - end - - end - end -end - -module ActionController #:nodoc: all - module Integration - class Session - include Shoulda::General - end + end end end + diff --git a/lib/shoulda/controller_tests/formats/html.rb b/lib/shoulda/controller_tests/formats/html.rb index d962e031..ba92bba8 100644 --- a/lib/shoulda/controller_tests/formats/html.rb +++ b/lib/shoulda/controller_tests/formats/html.rb @@ -1,194 +1,196 @@ -module Shoulda - module Controller # :nodoc: - module HTML # :nodoc: all - def self.included(other) - other.class_eval do - extend Shoulda::Controller::HTML::ClassMethods - end - end - - module ClassMethods - def controller_name_from_class - self.name.gsub(/Test/, '') - end - - def make_show_html_tests(res) - context "on GET to #{controller_name_from_class}#show" do - setup do - record = get_existing_record(res) - parent_params = make_parent_params(res, record) - get :show, parent_params.merge({ res.identifier => record.to_param }) - end - - if res.denied.actions.include?(:show) - should_not_assign_to res.object - should_redirect_to res.denied.redirect - should_set_the_flash_to res.denied.flash - else - should_assign_to res.object - should_respond_with :success - should_render_template :show - should_not_set_the_flash - end +module ThoughtBot # :nodoc: + module Shoulda # :nodoc: + module Controller # :nodoc: + module HTML # :nodoc: all + def self.included(other) + other.class_eval do + extend ThoughtBot::Shoulda::Controller::HTML::ClassMethods end end + + module ClassMethods + def controller_name_from_class + self.name.gsub(/Test/, '') + end - def make_edit_html_tests(res) - context "on GET to #{controller_name_from_class}#edit" do - setup do - @record = get_existing_record(res) - parent_params = make_parent_params(res, @record) - get :edit, parent_params.merge({ res.identifier => @record.to_param }) - end - - if res.denied.actions.include?(:edit) - should_not_assign_to res.object - should_redirect_to res.denied.redirect - should_set_the_flash_to res.denied.flash - else - should_assign_to res.object - should_respond_with :success - should_render_template :edit - should_not_set_the_flash - should_render_a_form - should "set @#{res.object} to requested instance" do - assert_equal @record, assigns(res.object) + def make_show_html_tests(res) + context "on GET to #{controller_name_from_class}#show" do + setup do + record = get_existing_record(res) + parent_params = make_parent_params(res, record) + get :show, parent_params.merge({ res.identifier => record.to_param }) end - end - end - end - def make_index_html_tests(res) - context "on GET to #{controller_name_from_class}#index" do - setup do - record = get_existing_record(res) rescue nil - parent_params = make_parent_params(res, record) - get(:index, parent_params) - end - - if res.denied.actions.include?(:index) - should_not_assign_to res.object.to_s.pluralize - should_redirect_to res.denied.redirect - should_set_the_flash_to res.denied.flash - else - should_respond_with :success - should_assign_to res.object.to_s.pluralize - should_render_template :index - should_not_set_the_flash - end - end - end - - def make_new_html_tests(res) - context "on GET to #{controller_name_from_class}#new" do - setup do - record = get_existing_record(res) rescue nil - parent_params = make_parent_params(res, record) - get(:new, parent_params) - end - - if res.denied.actions.include?(:new) - should_not_assign_to res.object - should_redirect_to res.denied.redirect - should_set_the_flash_to res.denied.flash - else - should_respond_with :success - should_assign_to res.object - should_not_set_the_flash - should_render_template :new - should_render_a_form - end - end - end - - def make_destroy_html_tests(res) - context "on DELETE to #{controller_name_from_class}#destroy" do - setup do - @record = get_existing_record(res) - parent_params = make_parent_params(res, @record) - delete :destroy, parent_params.merge({ res.identifier => @record.to_param }) - end - - if res.denied.actions.include?(:destroy) - should_redirect_to res.denied.redirect - should_set_the_flash_to res.denied.flash - - should "not destroy record" do - assert_nothing_raised { assert @record.reload } - end - else - should_set_the_flash_to res.destroy.flash - if res.destroy.redirect.is_a? Symbol - should_respond_with res.destroy.redirect + if res.denied.actions.include?(:show) + should_not_assign_to res.object + should_redirect_to res.denied.redirect + should_set_the_flash_to res.denied.flash else - should_redirect_to res.destroy.redirect + should_assign_to res.object + should_respond_with :success + should_render_template :show + should_not_set_the_flash end + end + end - should "destroy record" do - assert_raises(::ActiveRecord::RecordNotFound, "@#{res.object} was not destroyed.") do - @record.reload + def make_edit_html_tests(res) + context "on GET to #{controller_name_from_class}#edit" do + setup do + @record = get_existing_record(res) + parent_params = make_parent_params(res, @record) + get :edit, parent_params.merge({ res.identifier => @record.to_param }) + end + + if res.denied.actions.include?(:edit) + should_not_assign_to res.object + should_redirect_to res.denied.redirect + should_set_the_flash_to res.denied.flash + else + should_assign_to res.object + should_respond_with :success + should_render_template :edit + should_not_set_the_flash + should_render_a_form + should "set @#{res.object} to requested instance" do + assert_equal @record, assigns(res.object) end end end end - end - def make_create_html_tests(res) - context "on POST to #{controller_name_from_class}#create with #{res.create.params.inspect}" do - setup do - record = get_existing_record(res) rescue nil - parent_params = make_parent_params(res, record) - @count = res.klass.count - post :create, parent_params.merge(res.object => res.create.params) - end - - if res.denied.actions.include?(:create) - should_redirect_to res.denied.redirect - should_set_the_flash_to res.denied.flash - should_not_assign_to res.object - - should "not create new record" do - assert_equal @count, res.klass.count - end - else - should_assign_to res.object - should_set_the_flash_to res.create.flash - if res.create.redirect.is_a? Symbol - should_respond_with res.create.redirect + def make_index_html_tests(res) + context "on GET to #{controller_name_from_class}#index" do + setup do + record = get_existing_record(res) rescue nil + parent_params = make_parent_params(res, record) + get(:index, parent_params) + end + + if res.denied.actions.include?(:index) + should_not_assign_to res.object.to_s.pluralize + should_redirect_to res.denied.redirect + should_set_the_flash_to res.denied.flash else - should_redirect_to res.create.redirect + should_respond_with :success + should_assign_to res.object.to_s.pluralize + should_render_template :index + should_not_set_the_flash end - - should "not have errors on @#{res.object}" do - assert_equal [], pretty_error_messages(assigns(res.object)), "@#{res.object} has errors:" - end - end + end end - end - def make_update_html_tests(res) - context "on PUT to #{controller_name_from_class}#update with #{res.create.params.inspect}" do - setup do - @record = get_existing_record(res) - parent_params = make_parent_params(res, @record) - put :update, parent_params.merge(res.identifier => @record.to_param, res.object => res.update.params) - end - - if res.denied.actions.include?(:update) - should_not_assign_to res.object - should_redirect_to res.denied.redirect - should_set_the_flash_to res.denied.flash - else - should_assign_to res.object - should_set_the_flash_to(res.update.flash) - if res.update.redirect.is_a? Symbol - should_respond_with res.update.redirect - else - should_redirect_to res.update.redirect + def make_new_html_tests(res) + context "on GET to #{controller_name_from_class}#new" do + setup do + record = get_existing_record(res) rescue nil + parent_params = make_parent_params(res, record) + get(:new, parent_params) end - - should "not have errors on @#{res.object}" do - assert_equal [], pretty_error_messages(assigns(res.object)), "@#{res.object} has errors:" + + if res.denied.actions.include?(:new) + should_not_assign_to res.object + should_redirect_to res.denied.redirect + should_set_the_flash_to res.denied.flash + else + should_respond_with :success + should_assign_to res.object + should_not_set_the_flash + should_render_template :new + should_render_a_form + end + end + end + + def make_destroy_html_tests(res) + context "on DELETE to #{controller_name_from_class}#destroy" do + setup do + @record = get_existing_record(res) + parent_params = make_parent_params(res, @record) + delete :destroy, parent_params.merge({ res.identifier => @record.to_param }) + end + + if res.denied.actions.include?(:destroy) + should_redirect_to res.denied.redirect + should_set_the_flash_to res.denied.flash + + should "not destroy record" do + assert_nothing_raised { assert @record.reload } + end + else + should_set_the_flash_to res.destroy.flash + if res.destroy.redirect.is_a? Symbol + should_respond_with res.destroy.redirect + else + should_redirect_to res.destroy.redirect + end + + should "destroy record" do + assert_raises(::ActiveRecord::RecordNotFound, "@#{res.object} was not destroyed.") do + @record.reload + end + end + end + end + end + + def make_create_html_tests(res) + context "on POST to #{controller_name_from_class}#create with #{res.create.params.inspect}" do + setup do + record = get_existing_record(res) rescue nil + parent_params = make_parent_params(res, record) + @count = res.klass.count + post :create, parent_params.merge(res.object => res.create.params) + end + + if res.denied.actions.include?(:create) + should_redirect_to res.denied.redirect + should_set_the_flash_to res.denied.flash + should_not_assign_to res.object + + should "not create new record" do + assert_equal @count, res.klass.count + end + else + should_assign_to res.object + should_set_the_flash_to res.create.flash + if res.create.redirect.is_a? Symbol + should_respond_with res.create.redirect + else + should_redirect_to res.create.redirect + end + + should "not have errors on @#{res.object}" do + assert_equal [], pretty_error_messages(assigns(res.object)), "@#{res.object} has errors:" + end + end + end + end + + def make_update_html_tests(res) + context "on PUT to #{controller_name_from_class}#update with #{res.create.params.inspect}" do + setup do + @record = get_existing_record(res) + parent_params = make_parent_params(res, @record) + put :update, parent_params.merge(res.identifier => @record.to_param, res.object => res.update.params) + end + + if res.denied.actions.include?(:update) + should_not_assign_to res.object + should_redirect_to res.denied.redirect + should_set_the_flash_to res.denied.flash + else + should_assign_to res.object + should_set_the_flash_to(res.update.flash) + if res.update.redirect.is_a? Symbol + should_respond_with res.update.redirect + else + should_redirect_to res.update.redirect + end + + should "not have errors on @#{res.object}" do + assert_equal [], pretty_error_messages(assigns(res.object)), "@#{res.object} has errors:" + end end end end diff --git a/lib/shoulda/controller_tests/formats/xml.rb b/lib/shoulda/controller_tests/formats/xml.rb index 5fb4fbcd..f3c16f18 100644 --- a/lib/shoulda/controller_tests/formats/xml.rb +++ b/lib/shoulda/controller_tests/formats/xml.rb @@ -1,168 +1,170 @@ -module Shoulda - module Controller # :nodoc: - module XML - def self.included(other) #:nodoc: - other.class_eval do - extend Shoulda::Controller::XML::ClassMethods - end - end - - module ClassMethods - # Macro that creates a test asserting that the controller responded with an XML content-type - # and that the XML contains ++ as the root element. - def should_respond_with_xml_for(name = nil) - should "have ContentType set to 'application/xml'" do - assert_xml_response - end - - if name - should "return <#{name}/> as the root element" do - body = @response.body.first(100).map {|l| " #{l}"} - assert_select name.to_s.dasherize, 1, "Body:\n#{body}...\nDoes not have <#{name}/> as the root element." - end +module ThoughtBot # :nodoc: + module Shoulda # :nodoc: + module Controller # :nodoc: + module XML + def self.included(other) #:nodoc: + other.class_eval do + extend ThoughtBot::Shoulda::Controller::XML::ClassMethods end end - alias should_respond_with_xml should_respond_with_xml_for - - protected - - def make_show_xml_tests(res) # :nodoc: - context "on GET to #{controller_name_from_class}#show as xml" do - setup do - request_xml - record = get_existing_record(res) - parent_params = make_parent_params(res, record) - get :show, parent_params.merge({ res.identifier => record.to_param }) + + module ClassMethods + # Macro that creates a test asserting that the controller responded with an XML content-type + # and that the XML contains ++ as the root element. + def should_respond_with_xml_for(name = nil) + should "have ContentType set to 'application/xml'" do + assert_xml_response end - - if res.denied.actions.include?(:show) - should_not_assign_to res.object - should_respond_with 401 - else - should_assign_to res.object - should_respond_with :success - should_respond_with_xml_for res.object - end - end - end - - def make_edit_xml_tests(res) # :nodoc: - # XML doesn't need an :edit action - end - - def make_new_xml_tests(res) # :nodoc: - # XML doesn't need a :new action - end - - def make_index_xml_tests(res) # :nodoc: - context "on GET to #{controller_name_from_class}#index as xml" do - setup do - request_xml - parent_params = make_parent_params(res) - get(:index, parent_params) - end - - if res.denied.actions.include?(:index) - should_not_assign_to res.object.to_s.pluralize - should_respond_with 401 - else - should_respond_with :success - should_respond_with_xml_for res.object.to_s.pluralize - should_assign_to res.object.to_s.pluralize - end - end - end - - def make_destroy_xml_tests(res) # :nodoc: - context "on DELETE to #{controller_name_from_class}#destroy as xml" do - setup do - request_xml - @record = get_existing_record(res) - parent_params = make_parent_params(res, @record) - delete :destroy, parent_params.merge({ res.identifier => @record.to_param }) - end - - if res.denied.actions.include?(:destroy) - should_respond_with 401 - - should "not destroy record" do - assert @record.reload + + if name + should "return <#{name}/> as the root element" do + body = @response.body.first(100).map {|l| " #{l}"} + assert_select name.to_s.dasherize, 1, "Body:\n#{body}...\nDoes not have <#{name}/> as the root element." end - else - should "destroy record" do - assert_raises(::ActiveRecord::RecordNotFound, "@#{res.object} was not destroyed.") do - @record.reload + end + end + alias should_respond_with_xml should_respond_with_xml_for + + protected + + def make_show_xml_tests(res) # :nodoc: + context "on GET to #{controller_name_from_class}#show as xml" do + setup do + request_xml + record = get_existing_record(res) + parent_params = make_parent_params(res, record) + get :show, parent_params.merge({ res.identifier => record.to_param }) + end + + if res.denied.actions.include?(:show) + should_not_assign_to res.object + should_respond_with 401 + else + should_assign_to res.object + should_respond_with :success + should_respond_with_xml_for res.object + end + end + end + + def make_edit_xml_tests(res) # :nodoc: + # XML doesn't need an :edit action + end + + def make_new_xml_tests(res) # :nodoc: + # XML doesn't need a :new action + end + + def make_index_xml_tests(res) # :nodoc: + context "on GET to #{controller_name_from_class}#index as xml" do + setup do + request_xml + parent_params = make_parent_params(res) + get(:index, parent_params) + end + + if res.denied.actions.include?(:index) + should_not_assign_to res.object.to_s.pluralize + should_respond_with 401 + else + should_respond_with :success + should_respond_with_xml_for res.object.to_s.pluralize + should_assign_to res.object.to_s.pluralize + end + end + end + + def make_destroy_xml_tests(res) # :nodoc: + context "on DELETE to #{controller_name_from_class}#destroy as xml" do + setup do + request_xml + @record = get_existing_record(res) + parent_params = make_parent_params(res, @record) + delete :destroy, parent_params.merge({ res.identifier => @record.to_param }) + end + + if res.denied.actions.include?(:destroy) + should_respond_with 401 + + should "not destroy record" do + assert @record.reload + end + else + should "destroy record" do + assert_raises(::ActiveRecord::RecordNotFound, "@#{res.object} was not destroyed.") do + @record.reload + end + end + end + end + end + + def make_create_xml_tests(res) # :nodoc: + context "on POST to #{controller_name_from_class}#create as xml" do + setup do + request_xml + parent_params = make_parent_params(res) + @count = res.klass.count + post :create, parent_params.merge(res.object => res.create.params) + end + + if res.denied.actions.include?(:create) + should_respond_with 401 + should_not_assign_to res.object + + should "not create new record" do + assert_equal @count, res.klass.count + end + else + should_assign_to res.object + + should "not have errors on @#{res.object}" do + assert_equal [], pretty_error_messages(assigns(res.object)), "@#{res.object} has errors:" + end + end + end + end + + def make_update_xml_tests(res) # :nodoc: + context "on PUT to #{controller_name_from_class}#update as xml" do + setup do + request_xml + @record = get_existing_record(res) + parent_params = make_parent_params(res, @record) + put :update, parent_params.merge(res.identifier => @record.to_param, res.object => res.update.params) + end + + if res.denied.actions.include?(:update) + should_not_assign_to res.object + should_respond_with 401 + else + should_assign_to res.object + + should "not have errors on @#{res.object}" do + assert_equal [], assigns(res.object).errors.full_messages, "@#{res.object} has errors:" end end end end end - def make_create_xml_tests(res) # :nodoc: - context "on POST to #{controller_name_from_class}#create as xml" do - setup do - request_xml - parent_params = make_parent_params(res) - @count = res.klass.count - post :create, parent_params.merge(res.object => res.create.params) - end - - if res.denied.actions.include?(:create) - should_respond_with 401 - should_not_assign_to res.object + # Sets the next request's format to 'application/xml' + def request_xml + @request.accept = "application/xml" + end - should "not create new record" do - assert_equal @count, res.klass.count - end - else - should_assign_to res.object + # Asserts that the controller's response was 'application/xml' + def assert_xml_response + content_type = (@response.headers["Content-Type"] || @response.headers["type"]).to_s + regex = %r{\bapplication/xml\b} - should "not have errors on @#{res.object}" do - assert_equal [], pretty_error_messages(assigns(res.object)), "@#{res.object} has errors:" - end - end - end - end - - def make_update_xml_tests(res) # :nodoc: - context "on PUT to #{controller_name_from_class}#update as xml" do - setup do - request_xml - @record = get_existing_record(res) - parent_params = make_parent_params(res, @record) - put :update, parent_params.merge(res.identifier => @record.to_param, res.object => res.update.params) - end - - if res.denied.actions.include?(:update) - should_not_assign_to res.object - should_respond_with 401 - else - should_assign_to res.object - - should "not have errors on @#{res.object}" do - assert_equal [], assigns(res.object).errors.full_messages, "@#{res.object} has errors:" - end - end - end + msg = "Content Type '#{content_type.inspect}' doesn't match '#{regex.inspect}'\n" + msg += "Body: #{@response.body.first(100).chomp} ..." + + assert_match regex, content_type, msg end + end - - # Sets the next request's format to 'application/xml' - def request_xml - @request.accept = "application/xml" - end - - # Asserts that the controller's response was 'application/xml' - def assert_xml_response - content_type = (@response.headers["Content-Type"] || @response.headers["type"]).to_s - regex = %r{\bapplication/xml\b} - - msg = "Content Type '#{content_type.inspect}' doesn't match '#{regex.inspect}'\n" - msg += "Body: #{@response.body.first(100).chomp} ..." - - assert_match regex, content_type, msg - end - end end end diff --git a/lib/shoulda/extensions/proc.rb b/lib/shoulda/extensions/proc.rb deleted file mode 100644 index ec2c7d88..00000000 --- a/lib/shoulda/extensions/proc.rb +++ /dev/null @@ -1,17 +0,0 @@ -begin - require 'active_support' -rescue LoadError - # Stolen straight from ActiveSupport - class Proc #:nodoc: - def bind(object) - block, time = self, Time.now - (class << object; self end).class_eval do - method_name = "__bind_#{time.to_i}_#{time.usec}" - define_method(method_name, &block) - method = instance_method(method_name) - remove_method(method_name) - method - end.bind(object) - end - end -end diff --git a/lib/shoulda/gem/proc_extensions.rb b/lib/shoulda/gem/proc_extensions.rb new file mode 100644 index 00000000..0d577df9 --- /dev/null +++ b/lib/shoulda/gem/proc_extensions.rb @@ -0,0 +1,14 @@ +# Stolen straight from ActiveSupport + +class Proc #:nodoc: + def bind(object) + block, time = self, Time.now + (class << object; self end).class_eval do + method_name = "__bind_#{time.to_i}_#{time.usec}" + define_method(method_name, &block) + method = instance_method(method_name) + remove_method(method_name) + method + end.bind(object) + end +end diff --git a/lib/shoulda/gem/shoulda.rb b/lib/shoulda/gem/shoulda.rb new file mode 100644 index 00000000..d2293716 --- /dev/null +++ b/lib/shoulda/gem/shoulda.rb @@ -0,0 +1,241 @@ +require File.join(File.dirname(__FILE__), 'proc_extensions') + +module Thoughtbot + module Shoulda + class << self + attr_accessor :current_context + end + + VERSION = '1.1.1' + + # = Should statements + # + # Should statements are just syntactic sugar over normal Test::Unit test methods. A should block + # contains all the normal code and assertions you're used to seeing, with the added benefit that + # they can be wrapped inside context blocks (see below). + # + # == Example: + # + # class UserTest << Test::Unit::TestCase + # + # def setup + # @user = User.new("John", "Doe") + # end + # + # should "return its full name" + # assert_equal 'John Doe', @user.full_name + # end + # + # end + # + # ...will produce the following test: + # * "test: User should return its full name. " + # + # Note: The part before should in the test name is gleamed from the name of the Test::Unit class. + + def should(name, &blk) + should_eventually(name) && return unless block_given? + + if Shoulda.current_context + Shoulda.current_context.should(name, &blk) + else + context_name = self.name.gsub(/Test/, "") + context = Thoughtbot::Shoulda::Context.new(context_name, self) do + should(name, &blk) + end + context.build + end + end + + # Just like should, but never runs, and instead prints an 'X' in the Test::Unit output. + def should_eventually(name, &blk) + context_name = self.name.gsub(/Test/, "") + context = Thoughtbot::Shoulda::Context.new(context_name, self) do + should_eventually(name, &blk) + end + context.build + end + + # = Contexts + # + # A context block groups should statements under a common set of setup/teardown methods. + # Context blocks can be arbitrarily nested, and can do wonders for improving the maintainability + # and readability of your test code. + # + # A context block can contain setup, should, should_eventually, and teardown blocks. + # + # class UserTest << Test::Unit::TestCase + # 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 "test: A User instance should return its full name. ". + # + # Contexts may be nested. Nested contexts run their setup blocks from out to in before each + # should statement. They then run their teardown blocks from in to out after each should statement. + # + # class UserTest << Test::Unit::TestCase + # 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 + # * "test: A User instance should return its full name. " + # * "test: A User instance with a profile should return true when sent :has_profile?. " + # + # Just like should statements, a context block can exist next to normal def test_the_old_way; end + # tests. This means you do not have to fully commit to the context/should syntax in a test file. + + def context(name, &blk) + if Shoulda.current_context + Shoulda.current_context.context(name, &blk) + else + context = Thoughtbot::Shoulda::Context.new(name, self, &blk) + context.build + end + end + + class Context # :nodoc: + + attr_accessor :name # my name + attr_accessor :parent # may be another context, or the original test::unit class. + attr_accessor :subcontexts # array of contexts nested under myself + attr_accessor :setup_block # block given via a setup method + attr_accessor :teardown_block # block given via a teardown method + attr_accessor :shoulds # array of hashes representing the should statements + attr_accessor :should_eventuallys # array of hashes representing the should eventually statements + + def initialize(name, parent, &blk) + Shoulda.current_context = self + self.name = name + self.parent = parent + self.setup_block = nil + self.teardown_block = nil + self.shoulds = [] + self.should_eventuallys = [] + self.subcontexts = [] + + blk.bind(self).call + Shoulda.current_context = nil + end + + def context(name, &blk) + subcontexts << Context.new(name, self, &blk) + Shoulda.current_context = self + end + + def setup(&blk) + self.setup_block = blk + end + + def teardown(&blk) + self.teardown_block = blk + end + + def should(name, &blk) + self.shoulds << { :name => name, :block => blk } + end + + def should_eventually(name, &blk) + self.should_eventuallys << { :name => name, :block => blk } + end + + def full_name + parent_name = parent.full_name if am_subcontext? + return [parent_name, name].join(" ").strip + end + + def am_subcontext? + parent.is_a?(self.class) # my parent is the same class as myself. + end + + def test_unit_class + am_subcontext? ? parent.test_unit_class : parent + end + + def create_test_from_should_hash(should) + test_name = ["test:", full_name, "should", "#{should[:name]}. "].flatten.join(' ').to_sym + + if test_unit_class.instance_methods.include?(test_name.to_s) + warn " * WARNING: '#{test_name}' is already defined" + end + + context = self + test_unit_class.send(:define_method, test_name) do |*args| + begin + context.run_all_setup_blocks(self) + should[:block].bind(self).call + ensure + context.run_all_teardown_blocks(self) + end + end + end + + def run_all_setup_blocks(binding) + self.parent.run_all_setup_blocks(binding) if am_subcontext? + setup_block.bind(binding).call if setup_block + end + + def run_all_teardown_blocks(binding) + teardown_block.bind(binding).call if teardown_block + self.parent.run_all_teardown_blocks(binding) if am_subcontext? + end + + def print_should_eventuallys + should_eventuallys.each do |should| + test_name = [full_name, "should", "#{should[:name]}. "].flatten.join(' ') + puts " * DEFERRED: " + test_name + end + subcontexts.each { |context| context.print_should_eventuallys } + end + + def build + shoulds.each do |should| + create_test_from_should_hash(should) + end + + subcontexts.each { |context| context.build } + + print_should_eventuallys + end + + def method_missing(method, *args, &blk) + test_unit_class.send(method, *args, &blk) + end + + end + end +end + +module Test # :nodoc: all + module Unit + class TestCase + extend Thoughtbot::Shoulda + end + end +end + diff --git a/lib/shoulda/general.rb b/lib/shoulda/general.rb index ec350319..7b1b36b6 100644 --- a/lib/shoulda/general.rb +++ b/lib/shoulda/general.rb @@ -1,116 +1,118 @@ -module Shoulda - module General - def self.included(other) # :nodoc: - other.class_eval do - extend Shoulda::General::ClassMethods - end - end - - module ClassMethods - # Loads all fixture files (test/fixtures/*.yml) - def load_all_fixtures - all_fixtures = Dir.glob(File.join(Test::Unit::TestCase.fixture_path, "*.yml")).collect do |f| - File.basename(f, '.yml').to_sym +module ThoughtBot # :nodoc: + module Shoulda # :nodoc: + module General + def self.included(other) # :nodoc: + other.class_eval do + extend ThoughtBot::Shoulda::General::ClassMethods end - fixtures *all_fixtures end - end - - # Prints a message to stdout, tagged with the name of the calling method. - def report!(msg = "") - puts("#{caller.first}: #{msg}") - 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}.") } + + module ClassMethods + # Loads all fixture files (test/fixtures/*.yml) + def load_all_fixtures + all_fixtures = Dir.glob(File.join(Test::Unit::TestCase.fixture_path, "*.yml")).collect do |f| + File.basename(f, '.yml').to_sym + end + fixtures *all_fixtures + end + end + + # Prints a message to stdout, tagged with the name of the calling method. + def report!(msg = "") + puts("#{caller.first}: #{msg}") 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 } + # 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_equal(a1h, a2h, msg) - 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 } - # 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 - # assert_contains(['a', '1'], 'a') => passes - # assert_contains(['a', '1'], /not there/) => fails - def assert_contains(collection, x, extra_msg = "") - collection = [collection] unless collection.is_a?(Array) - msg = "#{x.inspect} 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.inspect} 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 object can be saved - # - # assert_save User.new(params) - def assert_save(obj) - assert obj.save, "Errors: #{pretty_error_messages obj}" - obj.reload - end - - # Asserts that the given object is valid - # - # assert_valid User.new(params) - def assert_valid(obj) - assert obj.valid?, "Errors: #{pretty_error_messages obj}" - end - - # Asserts that an email was delivered. Can take a block that can further - # narrow down the types of emails you're expecting. - # - # assert_sent_email - # - # Passes if ActionMailer::Base.deliveries has an email - # - # assert_sent_email do |email| - # email.subject =~ /hi there/ && email.to.include?('none@none.com') - # end - # - # Passes if there is an email with subject containing 'hi there' and - # 'none@none.com' as one of the recipients. - # - def assert_sent_email - emails = ActionMailer::Base.deliveries - assert !emails.empty?, "No emails were sent" - if block_given? - matching_emails = emails.select {|email| yield email } - assert !matching_emails.empty?, "None of the emails matched." + assert_equal(a1h, a2h, msg) end - end - # Asserts that no ActionMailer mails were delivered - # - # assert_did_not_send_email - def assert_did_not_send_email - msg = "Sent #{ActionMailer::Base.deliveries.size} emails.\n" - ActionMailer::Base.deliveries.each { |m| msg << " '#{m.subject}' sent to #{m.to.to_sentence}\n" } - assert ActionMailer::Base.deliveries.empty?, 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 + # assert_contains(['a', '1'], 'a') => passes + # assert_contains(['a', '1'], /not there/) => fails + def assert_contains(collection, x, extra_msg = "") + collection = [collection] unless collection.is_a?(Array) + msg = "#{x.inspect} 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 - def pretty_error_messages(obj) - obj.errors.map { |a, m| "#{a} #{m} (#{obj.send(a).inspect})" } + # 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.inspect} 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 object can be saved + # + # assert_save User.new(params) + def assert_save(obj) + assert obj.save, "Errors: #{pretty_error_messages obj}" + obj.reload + end + + # Asserts that the given object is valid + # + # assert_valid User.new(params) + def assert_valid(obj) + assert obj.valid?, "Errors: #{pretty_error_messages obj}" + end + + # Asserts that an email was delivered. Can take a block that can further + # narrow down the types of emails you're expecting. + # + # assert_sent_email + # + # Passes if ActionMailer::Base.deliveries has an email + # + # assert_sent_email do |email| + # email.subject =~ /hi there/ && email.to.include?('none@none.com') + # end + # + # Passes if there is an email with subject containing 'hi there' and + # 'none@none.com' as one of the recipients. + # + def assert_sent_email + emails = ActionMailer::Base.deliveries + assert !emails.empty?, "No emails were sent" + if block_given? + matching_emails = emails.select {|email| yield email } + assert !matching_emails.empty?, "None of the emails matched." + end + end + + # Asserts that no ActionMailer mails were delivered + # + # assert_did_not_send_email + def assert_did_not_send_email + msg = "Sent #{ActionMailer::Base.deliveries.size} emails.\n" + ActionMailer::Base.deliveries.each { |m| msg << " '#{m.subject}' sent to #{m.to.to_sentence}\n" } + assert ActionMailer::Base.deliveries.empty?, msg + end + + def pretty_error_messages(obj) + obj.errors.map { |a, m| "#{a} #{m} (#{obj.send(a).inspect})" } + end + end - end end diff --git a/lib/shoulda/private_helpers.rb b/lib/shoulda/private_helpers.rb index a7d8ecb6..6c14a24d 100644 --- a/lib/shoulda/private_helpers.rb +++ b/lib/shoulda/private_helpers.rb @@ -1,20 +1,22 @@ -module Shoulda - module Private # :nodoc: - # Returns the values for the entries in the args hash who's keys are listed in the wanted array. - # Will raise if there are keys in the args hash that aren't listed. - def get_options!(args, *wanted) - ret = [] - opts = (args.last.is_a?(Hash) ? args.pop : {}) - wanted.each {|w| ret << opts.delete(w)} - raise ArgumentError, "Unsupported options given: #{opts.keys.join(', ')}" unless opts.keys.empty? - return *ret - end +module ThoughtBot # :nodoc: + module Shoulda # :nodoc: + module Private # :nodoc: + # Returns the values for the entries in the args hash who's keys are listed in the wanted array. + # Will raise if there are keys in the args hash that aren't listed. + def get_options!(args, *wanted) + ret = [] + opts = (args.last.is_a?(Hash) ? args.pop : {}) + wanted.each {|w| ret << opts.delete(w)} + raise ArgumentError, "Unsupported options given: #{opts.keys.join(', ')}" unless opts.keys.empty? + return *ret + end - # Returns the model class constant, as determined by the test class name. - # - # class TestUser; model_class; end => User - def model_class - self.name.gsub(/Test$/, '').constantize + # Returns the model class constant, as determined by the test class name. + # + # class TestUser; model_class; end => User + def model_class + self.name.gsub(/Test$/, '').constantize + end end end end diff --git a/test/shoulda/context_test.rb b/test/other/context_test.rb similarity index 85% rename from test/shoulda/context_test.rb rename to test/other/context_test.rb index 2000e3d1..2a17e249 100644 --- a/test/shoulda/context_test.rb +++ b/test/other/context_test.rb @@ -64,4 +64,10 @@ class ContextTest < Test::Unit::TestCase # :nodoc: end end + should_eventually "should pass, since it's unimplemented" do + flunk "what?" + end + + should_eventually "should not require a block when using should_eventually" + should "should pass without a block, as that causes it to piggyback to should_eventually" end diff --git a/test/shoulda/helpers_test.rb b/test/other/helpers_test.rb similarity index 100% rename from test/shoulda/helpers_test.rb rename to test/other/helpers_test.rb diff --git a/test/shoulda/private_helpers_test.rb b/test/other/private_helpers_test.rb similarity index 93% rename from test/shoulda/private_helpers_test.rb rename to test/other/private_helpers_test.rb index f41dc8ac..9c359997 100644 --- a/test/shoulda/private_helpers_test.rb +++ b/test/other/private_helpers_test.rb @@ -1,7 +1,7 @@ require File.join(File.dirname(__FILE__), '..', 'test_helper') class PrivateHelpersTest < Test::Unit::TestCase # :nodoc: - include Shoulda::ActiveRecord + include ThoughtBot::Shoulda::ActiveRecord context "get_options!" do should "remove opts from args" do args = [:a, :b, {}] diff --git a/test/shoulda/shoulda_test.rb b/test/shoulda/shoulda_test.rb deleted file mode 100644 index bd26e06a..00000000 --- a/test/shoulda/shoulda_test.rb +++ /dev/null @@ -1,236 +0,0 @@ -require 'test/unit' -require 'rubygems' -require 'mocha' - -class ShouldaTest < Test::Unit::TestCase # :nodoc: - - should "be able to define a should statement outside of a context" do - assert true - end - - should "see the name of my class as ShouldaTest" do - assert_equal "ShouldaTest", self.class.name - end - - def self.should_see_class_methods - should "be able to see class methods" do - assert true - end - end - - def self.should_see_a_context_block_like_a_Test_Unit_class - should "see a context block as a Test::Unit class" do - assert_equal "ShouldaTest", self.class.name - end - end - - def self.should_see_blah - should "see @blah through a macro" do - assert @blah - end - end - - def self.should_not_see_blah - should "not see @blah through a macro" do - assert_nil @blah - end - end - - def self.should_be_able_to_make_context_macros(prefix = nil) - context "a macro" do - should "have the tests named correctly" do - assert_match(/^test: #{prefix}a macro should have the tests named correctly/, self.to_s) - end - end - end - - context "Context" do - - should_see_class_methods - should_see_a_context_block_like_a_Test_Unit_class - should_be_able_to_make_context_macros("Context ") - - should "not define @blah" do - assert ! self.instance_variables.include?("@blah") - end - - should_not_see_blah - - should "be able to define a should statement" do - assert true - end - - should "see the name of my class as ShouldaTest" do - assert_equal "ShouldaTest", self.class.name - end - - context "with a subcontext" do - should_be_able_to_make_context_macros("Context with a subcontext ") - end - end - - context "Context with setup block" do - setup do - @blah = "blah" - end - - should "have @blah == 'blah'" do - assert_equal "blah", @blah - end - should_see_blah - - should "have name set right" do - assert_match(/^test: Context with setup block/, self.to_s) - end - - context "and a subcontext" do - setup do - @blah = "#{@blah} twice" - end - - should "be named correctly" do - assert_match(/^test: Context with setup block and a subcontext should be named correctly/, self.to_s) - end - - should "run the setup methods in order" do - assert_equal @blah, "blah twice" - end - should_see_blah - end - end - - context "Another context with setup block" do - setup do - @blah = "foo" - end - - should "have @blah == 'foo'" do - assert_equal "foo", @blah - end - - should "have name set right" do - assert_match(/^test: Another context with setup block/, self.to_s) - end - should_see_blah - end - - should_eventually "pass, since it's a should_eventually" do - flunk "what?" - end - - should_eventually "should not require a block when using should_eventually" - should "should pass without a block, as that causes it to piggyback to should_eventually" - - # Context creation and naming - - def test_should_create_a_new_context - assert_nothing_raised do - Shoulda::Context.new("context name", self) do; end - end - end - - def test_should_create_a_nested_context - assert_nothing_raised do - parent = Shoulda::Context.new("Parent", self) do; end - child = Shoulda::Context.new("Child", parent) do; end - end - end - - def test_should_name_a_contexts_correctly - parent = Shoulda::Context.new("Parent", self) do; end - child = Shoulda::Context.new("Child", parent) do; end - grandchild = Shoulda::Context.new("GrandChild", child) do; end - - assert_equal "Parent", parent.full_name - assert_equal "Parent Child", child.full_name - assert_equal "Parent Child GrandChild", grandchild.full_name - end - - # Should statements - - def test_should_have_should_hashes_when_given_should_statements - context = Shoulda::Context.new("name", self) do - should "be good" do; end - should "another" do; end - end - - names = context.shoulds.map {|s| s[:name]} - assert_equal ["another", "be good"], names.sort - end - - # setup and teardown - - def test_should_capture_setup_and_teardown_blocks - context = Shoulda::Context.new("name", self) do - setup do; "setup"; end - teardown do; "teardown"; end - end - - assert_equal "setup", context.setup_block.call - assert_equal "teardown", context.teardown_block.call - end - - # building - - def test_should_create_shoulda_test_for_each_should_on_build - context = Shoulda::Context.new("name", self) do - should "one" do; end - should "two" do; end - end - context.expects(:create_test_from_should_hash).with(has_entry(:name => "one")) - context.expects(:create_test_from_should_hash).with(has_entry(:name => "two")) - context.build - end - - def test_should_create_test_methods_on_build - tu_class = Test::Unit::TestCase - context = Shoulda::Context.new("A Context", tu_class) do - should "define the test" do; end - end - - tu_class.expects(:define_method).with(:"test: A Context should define the test. ") - context.build - end - - def test_should_create_test_methods_on_build_when_subcontext - tu_class = Test::Unit::TestCase - context = Shoulda::Context.new("A Context", tu_class) do - context "with a child" do - should "define the test" do; end - end - end - - tu_class.expects(:define_method).with(:"test: A Context with a child should define the test. ") - context.build - end - - # Test::Unit integration - - def test_should_create_a_new_context_and_build_it_on_Test_Unit_context - c = mock("context") - c.expects(:build) - Shoulda::Context.expects(:new).with("foo", kind_of(Class)).returns(c) - self.class.context "foo" do; end - end - - def test_should_create_a_one_off_context_and_build_it_on_Test_Unit_should - s = mock("test") - Shoulda::Context.any_instance.expects(:should).with("rock").returns(s) - Shoulda::Context.any_instance.expects(:build) - self.class.should "rock" do; end - end - - def test_should_define_a_test_on_should - s = mock("test") - Shoulda::Context.any_instance.expects(:should).with("rock").returns(s) - Shoulda::Context.any_instance.expects(:build) - self.class.should "rock" do; end - end - - def test_should_create_a_one_off_context_and_build_it_on_Test_Unit_should_eventually - s = mock("test") - Shoulda::Context.any_instance.expects(:should_eventually).with("rock").returns(s) - Shoulda::Context.any_instance.expects(:build) - self.class.should_eventually "rock" do; end - end -end