From 7a4202f9b8fa77318694864af9d1bcfde4084deb Mon Sep 17 00:00:00 2001 From: tsaleh Date: Wed, 14 Mar 2007 18:12:55 +0000 Subject: [PATCH] Moved everthing to trunk git-svn-id: https://svn.thoughtbot.com/plugins/tb_test_helpers/trunk@38 7bbfaf0e-4d1d-0410-9690-a8bb5f8ef2aa --- README | 0 bin/convert_to_should_syntax | 40 +++++++ init.rb | 1 + lib/active_record_helpers.rb | 198 +++++++++++++++++++++++++++++++++++ lib/should.rb | 60 +++++++++++ lib/tb_test_helpers.rb | 48 +++++++++ test/context_test.rb | 63 +++++++++++ test/test_helper.rb | 6 ++ 8 files changed, 416 insertions(+) create mode 100644 README create mode 100755 bin/convert_to_should_syntax create mode 100644 init.rb create mode 100644 lib/active_record_helpers.rb create mode 100644 lib/should.rb create mode 100644 lib/tb_test_helpers.rb create mode 100644 test/context_test.rb create mode 100644 test/test_helper.rb diff --git a/README b/README new file mode 100644 index 00000000..e69de29b diff --git a/bin/convert_to_should_syntax b/bin/convert_to_should_syntax new file mode 100755 index 00000000..ca5b94da --- /dev/null +++ b/bin/convert_to_should_syntax @@ -0,0 +1,40 @@ +#!/usr/bin/env ruby +require 'fileutils' + +def usage(msg = nil) + puts "Error: #{msg}" if msg + puts if msg + puts "Usage: #{File.basename(__FILE__)} normal_test_file.rb" + puts + puts "Will convert an existing test file with names like " + puts + puts " def test_should_do_stuff" + puts " ..." + puts " end" + puts + puts "to one using the new syntax: " + puts + puts " should \"be super cool\" do" + puts " ..." + puts " end" + puts + puts "A copy of the old file will be left under /tmp/ in case this script just seriously screws up" + puts + exit (msg ? 2 : 0) +end + +usage("Wrong number of arguments.") unless ARGV.size == 1 +usage("This system doesn't have a /tmp directory. wtf?") unless File.directory?('/tmp') + +file = ARGV.shift +tmpfile = "/tmp/#{File.basename(file)}" +usage("File '#{file}' doesn't exist") unless File.exists?(file) + +FileUtils.cp(file, tmpfile) +contents = File.read(tmpfile) +contents.gsub!(/def test_should_(.*)\s*$/, 'should "\1" do') +contents.gsub!(/def test_(.*)\s*$/, 'should "RENAME ME: test \1" do') +contents.gsub!(/should ".*" do$/) {|line| line.tr!('_', ' ')} +File.open(file, 'w') { |f| f.write(contents) } + +puts "File '#{file}' has been converted to 'should' syntax. Old version has been stored in '#{tmpfile}'" diff --git a/init.rb b/init.rb new file mode 100644 index 00000000..b31b4f59 --- /dev/null +++ b/init.rb @@ -0,0 +1 @@ +require 'tb_test_helpers' \ No newline at end of file diff --git a/lib/active_record_helpers.rb b/lib/active_record_helpers.rb new file mode 100644 index 00000000..3e25c77f --- /dev/null +++ b/lib/active_record_helpers.rb @@ -0,0 +1,198 @@ +class Test::Unit::TestCase + class << self + + # Ensures that the model cannot be saved if one of the attributes listed is not present. + # Requires an existing record + def should_require_attributes(*attributes) + klass = self.name.gsub(/Test$/, '').constantize + attributes.each do |attribute| + should "require #{attribute} to be set" do + object = klass.new + assert !object.valid?, "Instance is still valid" + assert object.errors.on(attribute), "No errors found" + assert object.errors.on(attribute).to_a.include?("can't be blank"), "Error message doesn't match" + end + end + end + + # Ensures that the model cannot be saved if one of the attributes listed is not unique. + # Requires an existing record + def should_require_unique_attributes(*attributes) + klass = self.name.gsub(/Test$/, '').constantize + attributes.each do |attribute| + attribute = attribute.to_sym + should "require unique value for #{attribute}" do + assert existing = klass.find(:first), "Can't find first #{klass}" + object = klass.new + object.send(:"#{attribute}=", existing.send(attribute)) + assert !object.valid?, "Instance is still valid" + assert object.errors.on(attribute), "No errors found" + assert object.errors.on(attribute).to_a.include?('has already been taken'), "Error message doesn't match" + end + end + end + + # Ensures that the attribute cannot be set on update + # Requires an existing record + def should_protect_attributes(*attributes) + klass = self.name.gsub(/Test$/, '').constantize + attributes.each do |attribute| + attribute = attribute.to_sym + should "not allow #{attribute} to be changed by update" do + assert object = klass.find(:first), "Can't find first #{klass}" + value = object[attribute] + assert object.update_attributes({ attribute => 1 }), + "Cannot update #{klass} with { :#{attribute} => 1 }, #{object.errors.full_messages.to_sentence}" + assert object.valid?, "#{klass} isn't valid after changing #{attribute}" + assert_equal value, object[attribute], "Was able to change #{klass}##{attribute}" + end + end + end + + # Ensures that the attribute cannot be set to the given values + # Requires an existing record + def should_not_allow_values_for(attribute, *bad_values) + klass = self.name.gsub(/Test$/, '').constantize + bad_values.each do |v| + should "not allow #{attribute} to be set to \"#{v}\"" 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_match(/invalid/, object.errors.on(attribute), "Error set on #{attribute} doesn't include \"invalid\" when set to \"#{v}\"") + end + end + end + + # Ensures that the attribute can be set to the given values. + # Requires an existing record + def should_allow_values_for(attribute, *good_values) + klass = self.name.gsub(/Test$/, '').constantize + good_values.each do |v| + should "allow #{attribute} to be set to \"#{v}\"" do + assert object = klass.find(:first), "Can't find first #{klass}" + object.send("#{attribute}=", v) + object.save + # assert object.errors.on(attribute), "There are no errors set on #{attribute} after being set to \"#{v}\"" + assert_no_match(/invalid/, object.errors.on(attribute), "Error set on #{attribute} includes \"invalid\" when set to \"#{v}\"") + end + end + end + + # Ensures that the length of the attribute is in the given range + # Requires an existing record + def should_ensure_length_in_range(attribute, range) + klass = self.name.gsub(/Test$/, '').constantize + min_length = range.first + max_length = range.last + + min_value = "x" * (min_length - 1) + max_value = "x" * (max_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_match(/short/, object.errors.on(attribute), "Error set on #{attribute} doesn't include \"short\" when set to \"#{min_value}\"") + end + + should "not allow #{attribute} to be more than #{max_length} chars long" do + 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_match(/long/, object.errors.on(attribute), "Error set on #{attribute} doesn't include \"long\" when set to \"#{max_value}\"") + end + end + + # Ensure that the attribute is in the range specified + # Requires an existing record + def should_ensure_value_in_range(attribute, range) + klass = self.name.gsub(/Test$/, '').constantize + 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}\"" + 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}\"" + end + end + + # Ensure that the attribute is numeric + # Requires an existing record + def should_only_allow_numeric_values_for(*attributes) + klass = self.name.gsub(/Test$/, '').constantize + 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 object.errors.on(attribute), "No errors found" + assert object.errors.on(attribute).to_a.include?('is not a number'), "Error message doesn't match" + end + end + end + + # Ensures that the has_many relationship exists. + # The last parameter may be a hash of options. Currently, the only supported option + # is :through + def should_have_many(*associations) + opts = associations.last.is_a?(Hash) ? associations.pop : {} + klass = self.name.gsub(/Test$/, '').constantize + associations.each do |association| + should "have many #{association}#{" through #{opts[:through]}" if opts[:through]}" do + reflection = klass.reflect_on_association(association) + assert reflection + assert_equal :has_many, reflection.macro + assert_equal(opts[:through], reflection.options[:through]) if opts[:through] + end + end + end + + # Ensures that the has_and_belongs_to_many relationship exists. + def should_have_and_belong_to_many(*associations) + klass = self.name.gsub(/Test$/, '').constantize + associations.each do |association| + should "should have and belong to many #{association}" do + assert klass.reflect_on_association(association) + assert_equal :has_and_belongs_to_many, klass.reflect_on_association(association).macro + end + end + end + + # Ensure that the has_one relationship exists. + def should_have_one(*associations) + klass = self.name.gsub(/Test$/, '').constantize + associations.each do |association| + should "have one #{association}" do + assert klass.reflect_on_association(association) + assert_equal :has_one, klass.reflect_on_association(association).macro + end + end + end + + # Ensure that the belongs_to relationship exists. + def should_belong_to(*associations) + klass = self.name.gsub(/Test$/, '').constantize + associations.each do |association| + should "belong_to #{association}" do + assert klass.reflect_on_association(association) + assert_equal :belongs_to, klass.reflect_on_association(association).macro + end + end + end + end +end \ No newline at end of file diff --git a/lib/should.rb b/lib/should.rb new file mode 100644 index 00000000..a929a05d --- /dev/null +++ b/lib/should.rb @@ -0,0 +1,60 @@ +module TBTestHelpers + module Should + def Should.included(other) + @@_context_names = [] + @@_setup_blocks = [] + @@_teardown_blocks = [] + end + + def context(name, &context_block) + @@_context_names << name + context_block.bind(self).call + @@_context_names.pop + @@_setup_blocks.pop + @@_teardown_blocks.pop + end + + def setup(&setup_block) + @@_setup_blocks << setup_block + end + + 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) + unless @@_context_names.empty? + test_name = "test #{@@_context_names.join(" ")} should #{name}" + else + test_name = "test should #{name}" + end + test_name_sym = test_name.to_sym + + raise ArgumentError, "'#{test_name}' is already defined" and return if self.instance_methods.include? test_name + + setup_block = @@_setup_blocks.last + teardown_block = @@_teardown_blocks.last + + if opts[:unimplemented] + define_method test_name_sym 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_sym do |*args| + setup_block.bind(self).call if setup_block + should_block.bind(self).call(*args) + teardown_block.bind(self).call if teardown_block + end + end + end + + 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 new file mode 100644 index 00000000..0d39c30e --- /dev/null +++ b/lib/tb_test_helpers.rb @@ -0,0 +1,48 @@ +require 'active_record_helpers' +require 'should' + +class Test::Unit::TestCase + class << self + include TBTestHelpers::Should + + # Loads all fixture files + def load_all_fixtures + all_fixtures = Dir.glob(File.join(RAILS_ROOT, "test", "fixtures", "*.yml")).collect do |f| + File.basename(f, '.yml').to_sym + end + fixtures *all_fixtures + end + + end + + # Ensures that the number of items in the collection changes + def assert_difference(object, method, difference, reload = false) + initial_value = object.send(method) + yield + reload and object.send(:reload) + assert_equal initial_value + difference, object.send(method), "#{object}##{method} after block" + end + + # Ensures that object.method does not change + def assert_no_difference(object, method, reload = false, &block) + assert_difference(object, method, 0, reload, &block) + end + + def report!(msg = "") + @controller.logger.info("TESTING: #{caller.first}: #{msg}") + end + + # asserts that two arrays contain the same elements, the same number of times. Essentially ==, but unordered. + def assert_same_elements(a1, a2) + [:select, :inject, :size].each do |m| + [a1, a2].each {|a| assert_respond_to(a, m, "Are you sure that #{a} is an array?") } + end + + assert a1h = a1.inject({}){|h,e| h[e] = a1.select{|i| i == e}.size; h} + assert a2h = a2.inject({}){|h,e| h[e] = a2.select{|i| i == e}.size; h} + + assert_equal(a1, a2) + end + +end + diff --git a/test/context_test.rb b/test/context_test.rb new file mode 100644 index 00000000..25575147 --- /dev/null +++ b/test/context_test.rb @@ -0,0 +1,63 @@ +require File.join(File.dirname(__FILE__), 'test_helper') + +class ContextTest < Test::Unit::TestCase + + context "context with setup block" do + setup do + @blah = "blah" + end + + should "have @blah == 'blah'" do + assert_equal "blah", @blah + end + + should "have name set right" do + assert_match(/^test context with setup block/, self.to_s) + 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 + end + + context "context with method definition" do + setup do + def hello; "hi"; end + end + + should "be able to read that method" do + assert_equal "hi", hello + end + + should "have name set right" do + assert_match(/^test context with method definition/, self.to_s) + end + end + + context "final context" do + should "not define @blah" do + assert_nil @blah + end + + context "with subcontext" do + should "be named correctly" do + assert_match(/^test final context with subcontext should be named correctly/, self.to_s) + end + end + end + + should_eventually "pass anyway, since it's unimplemented" do + flunk "what?" + end + +end diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 00000000..6f614923 --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,6 @@ +$LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib') + +require 'rubygems' +require 'active_support' +require 'test/unit' +require 'tb_test_helpers'