Skip to content

Instantly share code, notes, and snippets.

@aslam
Forked from geekq/test_unit_plus.rb
Created September 6, 2010 09:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aslam/566837 to your computer and use it in GitHub Desktop.
Save aslam/566837 to your computer and use it in GitHub Desktop.
# Helps to organize and structure your tests with
# * human readable test names
# * flagging tests as pending
# * structuring the test implementation with blocks
# Please see the documentation of single methods below.
# Written by Vladimir Dobriakov
# See http://blog.geekq.net/2009/11/25/minimalist-testing-ruby/ for explanation
require 'test/unit'
class << Test::Unit::TestCase
attr_writer :ignore_failures
attr_writer :human_test_names
def ignore_failures
@ignore_failures ||= []
end
def human_test_names
@human_test_names ||= {}
end
# Creates a test method.
#
# @name [String] Your can use a human readable sentence to name your tests e.g.
# `test 'validation of Japanease VAT numbers` instead of having to write
# `def test_validation_of_japanease_vat_numbers`
#
# @options [Hash] Currently only :tdd option is supported. Imagine, your have
# created a test for the new, not yet existing functionality and would like
# to communicate your intentions to your coworker. So you want to commit the
# change. But you do not want to "break the build". And commenting out tests
# is really dirty - you want to track the progress, not sweep the dirt under
# the carpet. If you define your test like `test 'my test', :tdd => 'in_progress',`
# then the test is always executed and provides feedback but does not make
# the test suit fail.
#
# @block The body of your test. If you do not provide any, then test marked
# as pending.
def test(name, options={}, &block)
test_name = "test_#{name.gsub(' ','_')}"
raise ArgumentError, "#{test_name} is already defined" if
self.instance_methods.include? test_name.to_s
if block
define_method test_name, &block
if options[:tdd] # e.g. :tdd => 'in_progress'
ignore_failures << test_name
end
else
puts "PENDING: #{name}"
end
human_test_names[test_name] = name
end
# Defines test as pending. The test will NOT be executed.
# Use this instead of commenting out tests.
def xtest(name, options={}, &block)
puts "PENDING #{name}"
end
end
class Test::Unit::TestCase
# A hook. Override this method in your class to do something on every
# failure. E.g. for a web application you could store the content
# of the last response and show it in a browser.
# You can also augment the test log with additional data by returning
# additional message from your function.
# Example:
# def on_failure(e)
# if (last_response rescue false)
# if last_response.body.strip.empty?
# return ' [Empty response body]'
# else
# save_and_open_page if ENV['ASSERT_OPENS_PAGE'] and ENV['ASSERT_OPENS_PAGE']=='true'
# return ''
# end
# end
# end
def on_failure(e)
end
# Please set the VERBOSE_TEST environment variable to show single test steps
# somewhat similar to RSpec and stories.
def verbose_mode?
ENV['VERBOSE_TEST'] && ENV['VERBOSE_TEST'].downcase == 'true'
end
# Supports test driven development by enabling non-critical failures
# for features in_progress. No temporary uncommenting needed then.
# See also TestCase::test
def run(result)
method_name = name[/^[^(]+/]
name_to_show = self.class.human_test_names[method_name] || method_name
puts "\n\nTEST #{name_to_show } (#{self.class.name})\n" if verbose_mode?
yield(STARTED, name)
@_result = result
begin
setup
__send__(@method_name)
rescue Test::Unit::AssertionFailedError => e
msg = e.message
msg << on_failure(e).to_s
method_name = $1 if name =~ /(.*)\(/
if method_name and self.class.ignore_failures and self.class.ignore_failures.include? method_name
puts "\nNon-critical " +
Test::Unit::Failure.new(name, filter_backtrace(e.backtrace), msg).long_display
puts '--'
else
add_failure(msg, e.backtrace)
end
rescue Exception
raise if PASSTHROUGH_EXCEPTIONS.include? $!.class
add_error($!)
ensure
begin
teardown
rescue Test::Unit::AssertionFailedError => e
add_failure(e.message, e.backtrace)
rescue Exception
raise if PASSTHROUGH_EXCEPTIONS.include? $!.class
add_error($!)
end
end
result.add_run
yield(FINISHED, name)
end
# See also `expect`.
def prepare(msg)
puts "Prepare #{msg}\n" if verbose_mode?
yield if block_given?
end
# Allows for structuring the implementation of your tests - offers an
# abstraction level between the test method and implemenatation statements.
# You can also use it to fold your code in the editor.
# Extends the message of the failed assertions.
# See also `prepare`.
def expect(msg)
begin
puts "Expect #{msg}\n" if verbose_mode?
yield if block_given?
rescue Test::Unit::AssertionFailedError => e
extended = Test::Unit::AssertionFailedError.new("#{msg}: #{e.message}")
extended.set_backtrace e.backtrace
raise extended
end
end
# Makes the difference between hashes easy to grasp for humans.
# Compare it to the original implementation, which just prints both hashes
# after each other.
def assert_hashes_equal(expected, actual, msg=nil)
expected_keys = expected.keys.sort
actual_keys = actual.keys.sort
if !(too_many = actual_keys - expected_keys).empty?
flunk "Too many keys in the actual Hash: #{too_many.inspect}. #{msg}"
end
if !(missing = actual_keys - expected_keys).empty?
flunk "Missing keys in the actual Hash: #{missing.inspect}. #{msg}"
end
expected.each do |key, value|
assert_equal value, actual[key], "key '#{key}' #{msg}"
end
assert_equal expected, actual, msg
end
# Makes the difference between arrays easy to grasp for humans.
# Compare it to the original implementation, which just prints both arrays
# after each other.
def assert_arrays_equal(expected, actual, msg=nil)
assert_equal expected.length, actual.length, "Arrays of same length expected. #{msg}"
expected.each_with_index do |value, i|
assert_equal value, actual[i], "Array element #{i}. #{msg}"
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment