From b415bff884a3c25a058c2600f86da80d4fa28fde Mon Sep 17 00:00:00 2001 From: Tammer Saleh Date: Mon, 23 Jun 2008 12:36:31 -0400 Subject: [PATCH] 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