diff --git a/README b/README index d52257a8..c73d4b31 100644 --- a/README +++ b/README @@ -4,12 +4,13 @@ A collection of Test::Unit helper methods. Adds helpers for -#. Contexts and should statements -#. Common ActiveRecord model tests -#. A few general purpose assertions - +1. context and should statements +1. Common ActiveRecord model tests +1. A few general purpose assertions + == Todo -#. Controller test helpers -#. General code cleanups -#. More options for AR helpers +1. Controller test helpers +1. General code cleanups +1. More options for AR helpers + \ No newline at end of file diff --git a/Rakefile b/Rakefile index d567f3d8..53bc807f 100644 --- a/Rakefile +++ b/Rakefile @@ -8,6 +8,16 @@ Rake::TestTask.new do |t| t.verbose = true end +# Generate the RDoc documentation + +Rake::RDocTask.new { |rdoc| + rdoc.rdoc_dir = 'doc' + rdoc.title = "ThoughtBot Test Helpers -- Making your tests easy on the fingers and eyes" + rdoc.options << '--line-numbers' << '--inline-source' + rdoc.template = "#{ENV['template']}.rb" if ENV['template'] + rdoc.rdoc_files.include('README', 'lib/**/*.rb') +} + desc 'Default: run tests.' task :default => ['test'] diff --git a/lib/color.rb b/lib/color.rb index a6ae137d..36afa07d 100644 --- a/lib/color.rb +++ b/lib/color.rb @@ -1,19 +1,27 @@ require 'test/unit/ui/console/testrunner' # Completely stolen from redgreen gem -module Color - COLORS = { :clear => 0, :red => 31, :green => 32, :yellow => 33 } - def self.method_missing(color_name, *args) +# +# Adds colored output to your tests. Specify color: true in +# your test/tb_test_helpers.conf file to enable. +# +# *Bug*: for some reason, this adds another line of output to the end of +# every rake task, as though there was another (empty) set of tests. +# A fix would be most welcome. +# +module 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) end - def self.color(color) + def self.color(color) # :nodoc: "\e[#{COLORS[color.to_sym]}m" end end -module Test - module Unit - class TestResult +module Test # :nodoc: + module Unit # :nodoc: + class TestResult # :nodoc: alias :old_to_s :to_s def to_s if old_to_s =~ /\d+ tests, \d+ assertions, (\d+) failures, (\d+) errors/ @@ -22,7 +30,7 @@ module Test end end - class AutoRunner + class AutoRunner # :nodoc: alias :old_initialize :initialize def initialize(standalone) old_initialize(standalone) @@ -32,7 +40,7 @@ module Test end end - class Failure + class Failure # :nodoc: alias :old_long_display :long_display def long_display # old_long_display.sub('Failure', Color.red('Failure')) @@ -40,7 +48,7 @@ module Test end end - class Error + class Error # :nodoc: alias :old_long_display :long_display def long_display # old_long_display.sub('Error', Color.yellow('Error')) @@ -48,9 +56,9 @@ module Test end end - module UI - module Console - class RedGreenTestRunner < Test::Unit::UI::Console::TestRunner + module UI # :nodoc: + module Console # :nodoc: + class RedGreenTestRunner < Test::Unit::UI::Console::TestRunner # :nodoc: def output_single(something, level=NORMAL) return unless (output?(level)) something = case something diff --git a/lib/context.rb b/lib/context.rb new file mode 100644 index 00000000..c237dca8 --- /dev/null +++ b/lib/context.rb @@ -0,0 +1,120 @@ +module TBTestHelpers # :nodoc: + # = context and should blocks + # + # A context block can exist next to normal def test_blah statements, + # meaning you do not have to fully commit to the context/should syntax in a test file. We have been + # using this syntax at ThoughtBot, though, and find it very readable. + # + # A context block can contain setup, should, should_eventually, and teardown blocks. + # + # class UserTest << Test::Unit + # context "a User instance" do + # setup do + # @user = User.find(:first) + # end + # + # should "return its full name" + # assert_equal 'John Doe', @user.full_name + # end + # end + # end + # + # This code will produce the method "test a User instance should return its full name" (yes, with spaces in the name). + # + # Contexts may be nested. Nested contexts run their setup blocks from out to in before each test. + # They then run their teardown blocks from in to out after each test. + # + # class UserTest << Test::Unit + # context "a User instance" do + # setup do + # @user = User.find(:first) + # end + # + # should "return its full name" + # assert_equal 'John Doe', @user.full_name + # end + # + # context "with a profile" do + # setup do + # @user.profile = Profile.find(:first) + # end + # + # should "return true when sent :has_profile?" + # assert @user.has_profile? + # end + # end + # end + # end + # + # This code will produce the following methods + # * "test a User instance should return its full name" + # * "test a User instance with a profile should return true when sent :has_profile?" (which will have both setup blocks run before it.) + # + + module Context + def Context.included(other) # :nodoc: + @@context_names = [] + @@setup_blocks = [] + @@teardown_blocks = [] + end + + # Creates a context block with the given name. + def context(name, &context_block) + saved_setups = @@setup_blocks.dup + saved_teardowns = @@teardown_blocks.dup + saved_contexts = @@context_names.dup + + @@context_names << name + context_block.bind(self).call + + @@context_names = saved_contexts + @@setup_blocks = saved_setups + @@teardown_blocks = saved_teardowns + end + + # Run before every should block in the current context + def setup(&setup_block) + @@setup_blocks << setup_block + end + + # Run after every should block in the current context + def teardown(&teardown_block) + @@teardown_blocks << teardown_block + end + + # Defines a test. Can be called either inside our outside of a context. + # Optionally specify :unimplimented => true (see should_eventually) + def should(name, opts = {}, &should_block) + test_name = ["test", @@context_names, "should", "#{name}"].flatten.join(' ').to_sym + + name_defined = eval("self.instance_methods.include?('#{test_name.to_s.gsub(/['"]/, '\$1')}')", should_block.binding) + raise ArgumentError, "'#{test_name}' is already defined" and return if name_defined + + setup_blocks = @@setup_blocks.dup + teardown_blocks = @@teardown_blocks.dup + + if opts[:unimplemented] + define_method test_name do |*args| + # XXX find a better way of doing this. + assert true + STDOUT.putc "X" # Tests for this model are missing. + end + else + define_method test_name do |*args| + begin + setup_blocks.each {|b| b.bind(self).call } + should_block.bind(self).call(*args) + ensure + teardown_blocks.reverse.each {|b| b.bind(self).call } + end + end + end + end + + # Defines a specification that is not yet implemented. + # Will be displayed as an 'X' when running tests, and failures will not be shown. + def should_eventually(name, &block) + should("eventually #{name}", {:unimplemented => true}, &block) + end + end +end diff --git a/lib/should.rb b/lib/should.rb deleted file mode 100644 index 5384cec2..00000000 --- a/lib/should.rb +++ /dev/null @@ -1,66 +0,0 @@ -module TBTestHelpers # :nodoc: - module Should - def Should.included(other) # :nodoc: - @@context_names = [] - @@setup_blocks = [] - @@teardown_blocks = [] - end - - # Creates a context block with the given name. The context block can contain setup, should, should_eventually, and teardown blocks. - def context(name, &context_block) - saved_setups = @@setup_blocks.dup - saved_teardowns = @@teardown_blocks.dup - saved_contexts = @@context_names.dup - - @@context_names << name - context_block.bind(self).call - - @@context_names = saved_contexts - @@setup_blocks = saved_setups - @@teardown_blocks = saved_teardowns - end - - # Run before every should block in the current context - def setup(&setup_block) - @@setup_blocks << setup_block - end - - # Run after every should block in the current context - def teardown(&teardown_block) - @@teardown_blocks << teardown_block - end - - # Defines a specification. Can be called either inside our outside of a context. - def should(name, opts = {}, &should_block) - test_name = ["test", @@context_names, "should", "#{name}"].flatten.join(' ').to_sym - - name_defined = eval("self.instance_methods.include?('#{test_name.to_s.gsub(/['"]/, '\$1')}')", should_block.binding) - raise ArgumentError, "'#{test_name}' is already defined" and return if name_defined - - setup_blocks = @@setup_blocks.dup - teardown_blocks = @@teardown_blocks.dup - - if opts[:unimplemented] - define_method test_name do |*args| - # XXX find a better way of doing this. - assert true - STDOUT.putc "X" # Tests for this model are missing. - end - else - define_method test_name do |*args| - begin - setup_blocks.each {|b| b.bind(self).call } - should_block.bind(self).call(*args) - ensure - teardown_blocks.reverse.each {|b| b.bind(self).call } - end - end - end - end - - # Defines a specification that is not yet implemented. Will be displayed as an 'X' when running tests, and failures will not be shown. - def should_eventually(name, &block) - should("eventually #{name}", {:unimplemented => true}, &block) - end - end -end diff --git a/lib/tb_test_helpers.rb b/lib/tb_test_helpers.rb index 73dc0656..ae168ed1 100644 --- a/lib/tb_test_helpers.rb +++ b/lib/tb_test_helpers.rb @@ -1,5 +1,5 @@ require 'active_record_helpers' -require 'should' +require 'context' require 'yaml' config_file = "tb_test_helpers.conf" @@ -14,7 +14,7 @@ module Test # :nodoc: class << self include TBTestHelpers::Should - # Loads all fixture files + # Loads all fixture files (test/fixtures/*.yml) def load_all_fixtures all_fixtures = Dir.glob(File.join(RAILS_ROOT, "test", "fixtures", "*.yml")).collect do |f| File.basename(f, '.yml').to_sym @@ -24,12 +24,16 @@ module Test # :nodoc: end - # Logs a message, tagged with TESTING: and the name of the calling method. + # Prints a message to stdout, tagged with the name of the calling method. def report!(msg = "") puts("#{caller.first}: #{msg}") end # Ensures that the number of items in the collection changes + # assert_difference(User, :count, 1) { User.create } + # assert_difference(User.packages, :size, 3, true) { User.add_three_packages } + # + # Setting reload to true will call object.reload after the block (for ActiveRecord associations) def assert_difference(object, method, difference, reload = false, msg = nil) initial_value = object.send(method) yield @@ -37,12 +41,13 @@ module Test # :nodoc: assert_equal initial_value + difference, object.send(method), (msg || "#{object}##{method} after block") end - # Ensures that object.method does not change + # Ensures that object.method does not change. See assert_difference for usage. def assert_no_difference(object, method, reload = false, msg = nil, &block) assert_difference(object, method, 0, reload, msg, &block) end - # asserts that two arrays contain the same elements, the same number of times. Essentially ==, but unordered. + # 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}.") } @@ -54,27 +59,26 @@ module Test # :nodoc: assert_equal(a1h, a2h, msg) end + # Asserts that the given collection contains item x. If x is a regular expression, ensure that + # at least one element from the collection matches x. + # assert_contains(['a', '1'], /\d/) => passes def assert_contains(collection, x, extra_msg = "") collection = [collection] unless collection.is_a?(Array) msg = "#{x} not found in #{collection.to_a.inspect} " + extra_msg case x when Regexp: assert(collection.detect { |e| e =~ x }, msg) - when String: assert(collection.include?(x), msg) - when Fixnum: assert(collection.include?(x), msg) - else - raise ArgumentError, "Don't know what to do with #{x}" + else assert(collection.include?(x), msg) end end + # Asserts that the given collection does not contain item x. If x is a regular expression, ensure that + # none of the elements from the collection match x. def assert_does_not_contain(collection, x, extra_msg = "") collection = [collection] unless collection.is_a?(Array) msg = "#{x} found in #{collection.to_a.inspect} " + extra_msg case x when Regexp: assert(!collection.detect { |e| e =~ x }, msg) - when String: assert(!collection.include?(x), msg) - when Fixnum: assert(!collection.include?(x), msg) - else - raise ArgumentError, "Don't know what to do with #{x}" + else assert(!collection.include?(x), msg) end end