diff --git a/Rakefile b/Rakefile index 52c47ecf..4bb9a7ea 100644 --- a/Rakefile +++ b/Rakefile @@ -6,7 +6,7 @@ require 'rake/rdoctask' Rake::TestTask.new do |t| t.libs << 'lib' - t.pattern = 'test/{unit,functional,other}/**/*_test.rb' + t.pattern = 'test/**/*_test.rb' t.verbose = false end diff --git a/lib/shoulda.rb b/lib/shoulda.rb index cd571df2..e581977e 100644 --- a/lib/shoulda.rb +++ b/lib/shoulda.rb @@ -1,4 +1,4 @@ -require 'shoulda/gem/shoulda' +require 'shoulda/context' require 'shoulda/private_helpers' require 'shoulda/general' require 'shoulda/active_record_helpers' @@ -22,6 +22,125 @@ end require 'shoulda/color' if shoulda_options[:color] +module Shoulda + class << self + attr_accessor :current_context + end + + VERSION = '1.1.1' + + # = Should statements + # + # Should statements are just syntactic sugar over normal Test::Unit test methods. A should block + # contains all the normal code and assertions you're used to seeing, with the added benefit that + # they can be wrapped inside context blocks (see below). + # + # == Example: + # + # class UserTest << Test::Unit::TestCase + # + # def setup + # @user = User.new("John", "Doe") + # end + # + # should "return its full name" + # assert_equal 'John Doe', @user.full_name + # end + # + # end + # + # ...will produce the following test: + # * "test: User should return its full name. " + # + # Note: The part before should in the test name is gleamed from the name of the Test::Unit class. + + def should(name, &blk) + should_eventually(name) && return unless block_given? + + if Shoulda.current_context + Shoulda.current_context.should(name, &blk) + else + context_name = self.name.gsub(/Test/, "") + context = Shoulda::Context.new(context_name, self) do + should(name, &blk) + end + context.build + end + end + + # Just like should, but never runs, and instead prints an 'X' in the Test::Unit output. + def should_eventually(name, &blk) + context_name = self.name.gsub(/Test/, "") + context = Shoulda::Context.new(context_name, self) do + should_eventually(name, &blk) + end + context.build + end + + # = Contexts + # + # A context block groups should statements under a common set of setup/teardown methods. + # Context blocks can be arbitrarily nested, and can do wonders for improving the maintainability + # and readability of your test code. + # + # A context block can contain setup, should, should_eventually, and teardown blocks. + # + # class UserTest << Test::Unit::TestCase + # context "A User instance" do + # setup do + # @user = User.find(:first) + # end + # + # should "return its full name" + # assert_equal 'John Doe', @user.full_name + # end + # end + # end + # + # This code will produce the method "test: A User instance should return its full name. ". + # + # Contexts may be nested. Nested contexts run their setup blocks from out to in before each + # should statement. They then run their teardown blocks from in to out after each should statement. + # + # class UserTest << Test::Unit::TestCase + # context "A User instance" do + # setup do + # @user = User.find(:first) + # end + # + # should "return its full name" + # assert_equal 'John Doe', @user.full_name + # end + # + # context "with a profile" do + # setup do + # @user.profile = Profile.find(:first) + # end + # + # should "return true when sent :has_profile?" + # assert @user.has_profile? + # end + # end + # end + # end + # + # This code will produce the following methods + # * "test: A User instance should return its full name. " + # * "test: A User instance with a profile should return true when sent :has_profile?. " + # + # Just like should statements, a context block can exist next to normal def test_the_old_way; end + # tests. This means you do not have to fully commit to the context/should syntax in a test file. + + def context(name, &blk) + if Shoulda.current_context + Shoulda.current_context.context(name, &blk) + else + context = Shoulda::Context.new(name, self, &blk) + context.build + end + end +end + module Test # :nodoc: all module Unit class TestCase diff --git a/lib/shoulda/color.rb b/lib/shoulda/color.rb index 1324dd7b..170ae2c1 100644 --- a/lib/shoulda/color.rb +++ b/lib/shoulda/color.rb @@ -1,6 +1,8 @@ require 'test/unit/ui/console/testrunner' -# Completely stolen from redgreen gem +# Completely stolen from redgreen gem (thanks Pat Eyler and Chris Wanstrath). +# +# NOTE: Is this now in autotest? Can I remove this, then? # # Adds colored output to your tests. Specify color: true in # your ~/.shoulda.conf file to enable. diff --git a/lib/shoulda/context.rb b/lib/shoulda/context.rb new file mode 100644 index 00000000..333458ea --- /dev/null +++ b/lib/shoulda/context.rb @@ -0,0 +1,121 @@ +require File.join(File.dirname(__FILE__), 'extensions', 'proc') + +module Shoulda # :nodoc: + class Context + attr_accessor :name # my name + attr_accessor :parent # may be another context, or the original test::unit class. + attr_accessor :subcontexts # array of contexts nested under myself + attr_accessor :setup_block # block given via a setup method + attr_accessor :teardown_block # block given via a teardown method + attr_accessor :shoulds # array of hashes representing the should statements + attr_accessor :should_eventuallys # array of hashes representing the should eventually statements + + def initialize(name, parent, &blk) + Shoulda.current_context = self + self.name = name + self.parent = parent + self.setup_block = nil + self.teardown_block = nil + self.shoulds = [] + self.should_eventuallys = [] + self.subcontexts = [] + + blk.bind(self).call + Shoulda.current_context = nil + end + + def context(name, &blk) + subcontexts << Context.new(name, self, &blk) + Shoulda.current_context = self + end + + def setup(&blk) + self.setup_block = blk + end + + def teardown(&blk) + self.teardown_block = blk + end + + def should(name, &blk) + self.shoulds << { :name => name, :block => blk } + end + + def should_eventually(name, &blk) + self.should_eventuallys << { :name => name, :block => blk } + end + + def full_name + parent_name = parent.full_name if am_subcontext? + return [parent_name, name].join(" ").strip + end + + def am_subcontext? + parent.is_a?(self.class) # my parent is the same class as myself. + end + + def test_unit_class + am_subcontext? ? parent.test_unit_class : parent + end + + def create_test_from_should_hash(should) + test_name = ["test:", full_name, "should", "#{should[:name]}. "].flatten.join(' ').to_sym + + if test_unit_class.instance_methods.include?(test_name.to_s) + warn " * WARNING: '#{test_name}' is already defined" + end + + context = self + test_unit_class.send(:define_method, test_name) do |*args| + begin + context.run_all_setup_blocks(self) + should[:block].bind(self).call + ensure + context.run_all_teardown_blocks(self) + end + end + end + + def run_all_setup_blocks(binding) + self.parent.run_all_setup_blocks(binding) if am_subcontext? + setup_block.bind(binding).call if setup_block + end + + def run_all_teardown_blocks(binding) + teardown_block.bind(binding).call if teardown_block + self.parent.run_all_teardown_blocks(binding) if am_subcontext? + end + + def print_should_eventuallys + should_eventuallys.each do |should| + test_name = [full_name, "should", "#{should[:name]}. "].flatten.join(' ') + puts " * DEFERRED: " + test_name + end + subcontexts.each { |context| context.print_should_eventuallys } + end + + def build + shoulds.each do |should| + create_test_from_should_hash(should) + end + + subcontexts.each { |context| context.build } + + print_should_eventuallys + end + + def method_missing(method, *args, &blk) + test_unit_class.send(method, *args, &blk) + end + + end +end + +module Test # :nodoc: all + module Unit + class TestCase + extend Shoulda + end + end +end + diff --git a/lib/shoulda/extensions/proc.rb b/lib/shoulda/extensions/proc.rb new file mode 100644 index 00000000..ec2c7d88 --- /dev/null +++ b/lib/shoulda/extensions/proc.rb @@ -0,0 +1,17 @@ +begin + require 'active_support' +rescue LoadError + # Stolen straight from ActiveSupport + class Proc #:nodoc: + def bind(object) + block, time = self, Time.now + (class << object; self end).class_eval do + method_name = "__bind_#{time.to_i}_#{time.usec}" + define_method(method_name, &block) + method = instance_method(method_name) + remove_method(method_name) + method + end.bind(object) + end + end +end diff --git a/lib/shoulda/gem/proc_extensions.rb b/lib/shoulda/gem/proc_extensions.rb deleted file mode 100644 index 0d577df9..00000000 --- a/lib/shoulda/gem/proc_extensions.rb +++ /dev/null @@ -1,14 +0,0 @@ -# Stolen straight from ActiveSupport - -class Proc #:nodoc: - def bind(object) - block, time = self, Time.now - (class << object; self end).class_eval do - method_name = "__bind_#{time.to_i}_#{time.usec}" - define_method(method_name, &block) - method = instance_method(method_name) - remove_method(method_name) - method - end.bind(object) - end -end diff --git a/lib/shoulda/gem/shoulda.rb b/lib/shoulda/gem/shoulda.rb deleted file mode 100644 index 19163089..00000000 --- a/lib/shoulda/gem/shoulda.rb +++ /dev/null @@ -1,239 +0,0 @@ -require File.join(File.dirname(__FILE__), 'proc_extensions') - -module Shoulda - class << self - attr_accessor :current_context - end - - VERSION = '1.1.1' - - # = Should statements - # - # Should statements are just syntactic sugar over normal Test::Unit test methods. A should block - # contains all the normal code and assertions you're used to seeing, with the added benefit that - # they can be wrapped inside context blocks (see below). - # - # == Example: - # - # class UserTest << Test::Unit::TestCase - # - # def setup - # @user = User.new("John", "Doe") - # end - # - # should "return its full name" - # assert_equal 'John Doe', @user.full_name - # end - # - # end - # - # ...will produce the following test: - # * "test: User should return its full name. " - # - # Note: The part before should in the test name is gleamed from the name of the Test::Unit class. - - def should(name, &blk) - should_eventually(name) && return unless block_given? - - if Shoulda.current_context - Shoulda.current_context.should(name, &blk) - else - context_name = self.name.gsub(/Test/, "") - context = Shoulda::Context.new(context_name, self) do - should(name, &blk) - end - context.build - end - end - - # Just like should, but never runs, and instead prints an 'X' in the Test::Unit output. - def should_eventually(name, &blk) - context_name = self.name.gsub(/Test/, "") - context = Shoulda::Context.new(context_name, self) do - should_eventually(name, &blk) - end - context.build - end - - # = Contexts - # - # A context block groups should statements under a common set of setup/teardown methods. - # Context blocks can be arbitrarily nested, and can do wonders for improving the maintainability - # and readability of your test code. - # - # A context block can contain setup, should, should_eventually, and teardown blocks. - # - # class UserTest << Test::Unit::TestCase - # context "A User instance" do - # setup do - # @user = User.find(:first) - # end - # - # should "return its full name" - # assert_equal 'John Doe', @user.full_name - # end - # end - # end - # - # This code will produce the method "test: A User instance should return its full name. ". - # - # Contexts may be nested. Nested contexts run their setup blocks from out to in before each - # should statement. They then run their teardown blocks from in to out after each should statement. - # - # class UserTest << Test::Unit::TestCase - # context "A User instance" do - # setup do - # @user = User.find(:first) - # end - # - # should "return its full name" - # assert_equal 'John Doe', @user.full_name - # end - # - # context "with a profile" do - # setup do - # @user.profile = Profile.find(:first) - # end - # - # should "return true when sent :has_profile?" - # assert @user.has_profile? - # end - # end - # end - # end - # - # This code will produce the following methods - # * "test: A User instance should return its full name. " - # * "test: A User instance with a profile should return true when sent :has_profile?. " - # - # Just like should statements, a context block can exist next to normal def test_the_old_way; end - # tests. This means you do not have to fully commit to the context/should syntax in a test file. - - def context(name, &blk) - if Shoulda.current_context - Shoulda.current_context.context(name, &blk) - else - context = Shoulda::Context.new(name, self, &blk) - context.build - end - end - - class Context # :nodoc: - - attr_accessor :name # my name - attr_accessor :parent # may be another context, or the original test::unit class. - attr_accessor :subcontexts # array of contexts nested under myself - attr_accessor :setup_block # block given via a setup method - attr_accessor :teardown_block # block given via a teardown method - attr_accessor :shoulds # array of hashes representing the should statements - attr_accessor :should_eventuallys # array of hashes representing the should eventually statements - - def initialize(name, parent, &blk) - Shoulda.current_context = self - self.name = name - self.parent = parent - self.setup_block = nil - self.teardown_block = nil - self.shoulds = [] - self.should_eventuallys = [] - self.subcontexts = [] - - blk.bind(self).call - Shoulda.current_context = nil - end - - def context(name, &blk) - subcontexts << Context.new(name, self, &blk) - Shoulda.current_context = self - end - - def setup(&blk) - self.setup_block = blk - end - - def teardown(&blk) - self.teardown_block = blk - end - - def should(name, &blk) - self.shoulds << { :name => name, :block => blk } - end - - def should_eventually(name, &blk) - self.should_eventuallys << { :name => name, :block => blk } - end - - def full_name - parent_name = parent.full_name if am_subcontext? - return [parent_name, name].join(" ").strip - end - - def am_subcontext? - parent.is_a?(self.class) # my parent is the same class as myself. - end - - def test_unit_class - am_subcontext? ? parent.test_unit_class : parent - end - - def create_test_from_should_hash(should) - test_name = ["test:", full_name, "should", "#{should[:name]}. "].flatten.join(' ').to_sym - - if test_unit_class.instance_methods.include?(test_name.to_s) - warn " * WARNING: '#{test_name}' is already defined" - end - - context = self - test_unit_class.send(:define_method, test_name) do |*args| - begin - context.run_all_setup_blocks(self) - should[:block].bind(self).call - ensure - context.run_all_teardown_blocks(self) - end - end - end - - def run_all_setup_blocks(binding) - self.parent.run_all_setup_blocks(binding) if am_subcontext? - setup_block.bind(binding).call if setup_block - end - - def run_all_teardown_blocks(binding) - teardown_block.bind(binding).call if teardown_block - self.parent.run_all_teardown_blocks(binding) if am_subcontext? - end - - def print_should_eventuallys - should_eventuallys.each do |should| - test_name = [full_name, "should", "#{should[:name]}. "].flatten.join(' ') - puts " * DEFERRED: " + test_name - end - subcontexts.each { |context| context.print_should_eventuallys } - end - - def build - shoulds.each do |should| - create_test_from_should_hash(should) - end - - subcontexts.each { |context| context.build } - - print_should_eventuallys - end - - def method_missing(method, *args, &blk) - test_unit_class.send(method, *args, &blk) - end - - end -end - -module Test # :nodoc: all - module Unit - class TestCase - extend Shoulda - end - end -end - diff --git a/test/other/context_test.rb b/test/shoulda/context_test.rb similarity index 85% rename from test/other/context_test.rb rename to test/shoulda/context_test.rb index 2a17e249..2000e3d1 100644 --- a/test/other/context_test.rb +++ b/test/shoulda/context_test.rb @@ -64,10 +64,4 @@ class ContextTest < Test::Unit::TestCase # :nodoc: end end - should_eventually "should pass, since it's unimplemented" do - flunk "what?" - end - - should_eventually "should not require a block when using should_eventually" - should "should pass without a block, as that causes it to piggyback to should_eventually" end diff --git a/test/other/helpers_test.rb b/test/shoulda/helpers_test.rb similarity index 100% rename from test/other/helpers_test.rb rename to test/shoulda/helpers_test.rb diff --git a/test/other/private_helpers_test.rb b/test/shoulda/private_helpers_test.rb similarity index 100% rename from test/other/private_helpers_test.rb rename to test/shoulda/private_helpers_test.rb diff --git a/test/shoulda/shoulda_test.rb b/test/shoulda/shoulda_test.rb new file mode 100644 index 00000000..bd26e06a --- /dev/null +++ b/test/shoulda/shoulda_test.rb @@ -0,0 +1,236 @@ +require 'test/unit' +require 'rubygems' +require 'mocha' + +class ShouldaTest < Test::Unit::TestCase # :nodoc: + + should "be able to define a should statement outside of a context" do + assert true + end + + should "see the name of my class as ShouldaTest" do + assert_equal "ShouldaTest", self.class.name + end + + def self.should_see_class_methods + should "be able to see class methods" do + assert true + end + end + + def self.should_see_a_context_block_like_a_Test_Unit_class + should "see a context block as a Test::Unit class" do + assert_equal "ShouldaTest", self.class.name + end + end + + def self.should_see_blah + should "see @blah through a macro" do + assert @blah + end + end + + def self.should_not_see_blah + should "not see @blah through a macro" do + assert_nil @blah + end + end + + def self.should_be_able_to_make_context_macros(prefix = nil) + context "a macro" do + should "have the tests named correctly" do + assert_match(/^test: #{prefix}a macro should have the tests named correctly/, self.to_s) + end + end + end + + context "Context" do + + should_see_class_methods + should_see_a_context_block_like_a_Test_Unit_class + should_be_able_to_make_context_macros("Context ") + + should "not define @blah" do + assert ! self.instance_variables.include?("@blah") + end + + should_not_see_blah + + should "be able to define a should statement" do + assert true + end + + should "see the name of my class as ShouldaTest" do + assert_equal "ShouldaTest", self.class.name + end + + context "with a subcontext" do + should_be_able_to_make_context_macros("Context with a subcontext ") + end + end + + context "Context with setup block" do + setup do + @blah = "blah" + end + + should "have @blah == 'blah'" do + assert_equal "blah", @blah + end + should_see_blah + + should "have name set right" do + assert_match(/^test: Context with setup block/, self.to_s) + end + + context "and a subcontext" do + setup do + @blah = "#{@blah} twice" + end + + should "be named correctly" do + assert_match(/^test: Context with setup block and a subcontext should be named correctly/, self.to_s) + end + + should "run the setup methods in order" do + assert_equal @blah, "blah twice" + end + should_see_blah + end + end + + context "Another context with setup block" do + setup do + @blah = "foo" + end + + should "have @blah == 'foo'" do + assert_equal "foo", @blah + end + + should "have name set right" do + assert_match(/^test: Another context with setup block/, self.to_s) + end + should_see_blah + end + + should_eventually "pass, since it's a should_eventually" do + flunk "what?" + end + + should_eventually "should not require a block when using should_eventually" + should "should pass without a block, as that causes it to piggyback to should_eventually" + + # Context creation and naming + + def test_should_create_a_new_context + assert_nothing_raised do + Shoulda::Context.new("context name", self) do; end + end + end + + def test_should_create_a_nested_context + assert_nothing_raised do + parent = Shoulda::Context.new("Parent", self) do; end + child = Shoulda::Context.new("Child", parent) do; end + end + end + + def test_should_name_a_contexts_correctly + parent = Shoulda::Context.new("Parent", self) do; end + child = Shoulda::Context.new("Child", parent) do; end + grandchild = Shoulda::Context.new("GrandChild", child) do; end + + assert_equal "Parent", parent.full_name + assert_equal "Parent Child", child.full_name + assert_equal "Parent Child GrandChild", grandchild.full_name + end + + # Should statements + + def test_should_have_should_hashes_when_given_should_statements + context = Shoulda::Context.new("name", self) do + should "be good" do; end + should "another" do; end + end + + names = context.shoulds.map {|s| s[:name]} + assert_equal ["another", "be good"], names.sort + end + + # setup and teardown + + def test_should_capture_setup_and_teardown_blocks + context = Shoulda::Context.new("name", self) do + setup do; "setup"; end + teardown do; "teardown"; end + end + + assert_equal "setup", context.setup_block.call + assert_equal "teardown", context.teardown_block.call + end + + # building + + def test_should_create_shoulda_test_for_each_should_on_build + context = Shoulda::Context.new("name", self) do + should "one" do; end + should "two" do; end + end + context.expects(:create_test_from_should_hash).with(has_entry(:name => "one")) + context.expects(:create_test_from_should_hash).with(has_entry(:name => "two")) + context.build + end + + def test_should_create_test_methods_on_build + tu_class = Test::Unit::TestCase + context = Shoulda::Context.new("A Context", tu_class) do + should "define the test" do; end + end + + tu_class.expects(:define_method).with(:"test: A Context should define the test. ") + context.build + end + + def test_should_create_test_methods_on_build_when_subcontext + tu_class = Test::Unit::TestCase + context = Shoulda::Context.new("A Context", tu_class) do + context "with a child" do + should "define the test" do; end + end + end + + tu_class.expects(:define_method).with(:"test: A Context with a child should define the test. ") + context.build + end + + # Test::Unit integration + + def test_should_create_a_new_context_and_build_it_on_Test_Unit_context + c = mock("context") + c.expects(:build) + Shoulda::Context.expects(:new).with("foo", kind_of(Class)).returns(c) + self.class.context "foo" do; end + end + + def test_should_create_a_one_off_context_and_build_it_on_Test_Unit_should + s = mock("test") + Shoulda::Context.any_instance.expects(:should).with("rock").returns(s) + Shoulda::Context.any_instance.expects(:build) + self.class.should "rock" do; end + end + + def test_should_define_a_test_on_should + s = mock("test") + Shoulda::Context.any_instance.expects(:should).with("rock").returns(s) + Shoulda::Context.any_instance.expects(:build) + self.class.should "rock" do; end + end + + def test_should_create_a_one_off_context_and_build_it_on_Test_Unit_should_eventually + s = mock("test") + Shoulda::Context.any_instance.expects(:should_eventually).with("rock").returns(s) + Shoulda::Context.any_instance.expects(:build) + self.class.should_eventually "rock" do; end + end +end