From: Patrick May Date: 2005-05-15T09:04:21+09:00 Subject: assert_includes Hello, Narf [1] has a useful assert for hashes and arrays. Given a structure: ahash = { "key1" => "value1", "key2" => "value2" } you can run: assert_includes( ahash, "key1" => "value1" ) and succeed. We found this immediately useful when writing asserts on form values, but it may a generally useful assert. Perhaps more useful, it also supports object methods as well, as in: class Foo def method1 "value1" end def method2 "value2" end def method3 "value3" end end assert_includes( Foo.new, "method1" => "value1", "method3" => "value3" ) I've found it to be a nice way to quickly check the part of an object's state. Attached is the implementation with tests. This code was developed by Tom Clarke as a part of Narf, it is available under Ruby's license (as is Narf). Cheers, Patrick 1. http://www.narf-lib.org ----------------------------------------- class TestHashCompare < Test::Unit::TestCase def test_includes_same assert_includes( {"a" => "aa"} , {"a" => "aa"} ) end def test_includes_missing e = assert_raises( Test::Unit::AssertionFailedError ) { assert_includes( { "a" => "aa", "b" => "bb" } , {"a" => "aa"} ) } assert e.to_s.index( "Missing from actual: b => \"bb\"" ) end def test_includes_different e = assert_raises( Test::Unit::AssertionFailedError ) { assert_includes( {"a" => "aa"} , { "a" => "bb" } ) } assert e.to_s.index("Difference: required \"aa\"> was \"bb\">" ) end def test_includes_multi_level e = assert_raises( Test::Unit::AssertionFailedError ) { assert_includes( {"a" => { "a" => "aa", "b" => "bb" }} , { "a" => { "a" => "aa", "b" => "cc"}} ) } assert e.to_s.index("Difference: required \"bb\"> was \"cc\">") end def test_includes_array_with_difference e = assert_raises( Test::Unit::AssertionFailedError ) { assert_includes( {"a" => [{ "a" => "aa" }, {"b" => "bb" }]},{ "a" => [ {"a" => "aa"}, {"b" => "cc"}]}) } assert e.to_s.index("Difference: required \"bb\"> was \"cc\">") end def test_includes_array_with_items_missing e = assert_raises( Test::Unit::AssertionFailedError ) { assert_includes( { "a" => [ {"a" => "aa"}, {"b" => "cc"}]} , {"a" => [{ "a" => "aa" }]} ) } assert e.to_s.index("Missing from actual: a[1] => {\"b\"=>\"cc\"}") end def test_includes_hash_when_should_not e = assert_raises( Test::Unit::AssertionFailedError ) { assert_includes( {"a" => "b"} , { "a" => { "a" => "aa", "b" => "cc"}} ) } assert e.to_s.index("Difference: required \"b\"> was {\"a\"=>\"aa\", \"b\"=>\"cc\"}>") end def test_does_not_include_hash_when_it_should e = assert_raises( Test::Unit::AssertionFailedError ) { assert_includes( { "a" => { "a" => "aa" , "b" => "cc"}}, {"a" => "b"} ) } assert e.to_s.index("Difference: required {\"a\"=>\"aa\", \"b\"=>\"cc\"}> was \"b\">") end def test_includes_array_when_should_not e = assert_raises( Test::Unit::AssertionFailedError ) { assert_includes( {"a" => "a"}, { "a" => [ {"a" => "aa"}, {"b" => "cc"}]} ) } assert e.to_s.index("Difference: required \"a\"> was [{\"a\"=>\"aa\"}, {\"b\"=>\"cc\"}]>") end def test_does_not_include_array_when_it_should e = assert_raises( Test::Unit::AssertionFailedError ) { assert_includes( {"a" => "b"} , { "a" => { "a" => "aa" , "b" => "cc"}} ) } assert e.to_s.index("Difference: required \"b\"> was {\"a\"=>\"aa\", \"b\"=>\"cc\"}>" ) end def test_assert_includes assert_includes( {"a" => { "a" => "aa" }}, { "a" => { "a" => "aa" , "b" => "aa"}} ) end def test_assert_includes2 assert_raises (Test::Unit::AssertionFailedError) { assert_includes( { "a" => { "a" => "aa" , "b" => "cc"}}, {"a" => { "c" => "bb" },"b" => { "c" => "bb" }}) } end def test_assert_includes_array assert_includes( ["a"],[ "a","b"] ) end def test_demonstrate_failure assert_raises (Test::Unit::AssertionFailedError) { assert_includes(["a","b"],[ "a"]) } end class Trivial attr_accessor :name def initialize( name ) @name = name end end def test_compare_object_tree_to_hash assert_includes({"name" => "patrick"}, Trivial.new("patrick")) end def test_compare_object_tree_to_hash assert_includes({"name" => { "name" => "patrick" }}, Trivial.new(Trivial.new("patrick"))) end # TODO: # reports an error if either item is not a hash # improve error reporting when comparing a hash to an array and vice versa # add an enhanced version of assert equals end class Array # :nodoc: def has_key?(index) index >= 0 && index < length end def __index(key) self[key] end def compare_includes? haystack, prefix=[] message = "" each_with_index{ |v, k| fullname = prefix.clone if fullname.length > 0 fullname[fullname.length-1] = "#{fullname.last}[#{k}]" else fullname[0] = "[#{k}]" end fullname_str = fullname.join(".") message += do_thing(haystack, k, v, fullname, fullname_str) } message end def assert_includes( needle, message="" ) #assert_includes( self, needle, message ) message = needle.compare_includes? self unless message == "" raise Test::Unit::AssertionFailedError.new(message) #flunk( message ) end end end class Object # :nodoc: def has_key? key respond_to? key.intern end def __index(key) send key.intern end def do_thing haystack, k, v, fullname, fullname_str message = "" # get key if !haystack.has_key?(k) if v message += "Missing from actual: #{fullname_str} => #{v.inspect}\n" end else # inspect key # if we are looking for a collection if v.kind_of?(Hash) || v.kind_of?(Array) # and the haystack has a collection if !(haystack.__index(k).kind_of?(String) || haystack.__index(k).kind_of?(TrueClass) || haystack.__index(k).kind_of?(FalseClass) || haystack.__index(k).kind_of?(Fixnum) || haystack.__index(k).kind_of?(Bignum)) # recurse if both are collections message += v.compare_includes?(haystack.__index(k),fullname) # otherwise, complain that we aren't the same else message += "Difference: required <#{fullname_str} => #{v.inspect}> was <#{fullname_str} => #{haystack.__index(k).inspect}>\n" end # if we aren't looking for a collection, see if we aren't the same else haystack_value = haystack.__index(k) if (haystack_value.kind_of? Array and haystack_value.length == 1) haystack_value = haystack_value.first end if (haystack_value != v) message += "Difference: required <#{fullname_str} => #{v.inspect}> was <#{fullname_str} => #{haystack_value.inspect}>\n" end end end message end end class Hash # :nodoc: def __index(key) self[key] end def compare_includes? haystack, prefix=[] message = "" # check that haystack is the same type? # iterate over your self (particular to self) self.each do |k,v| # make a sensible name for each element of the needle (particular to self) fullname = prefix.clone.push k fullname_str = fullname.join(".") message += do_thing(haystack, k, v, fullname, fullname_str) end message end def assert_includes( needle, message="" ) message = needle.compare_includes? self unless message == "" raise Test::Unit::AssertionFailedError.new(message) #flunk( message ) end end end module Test # :nodoc: all module Unit module Assertions def assert_includes needle, haystack, message="" _wrap_assertion { message = needle.compare_includes?( haystack ) unless( message == "") raise Test::Unit::AssertionFailedError.new(message.chomp) #flunk(message.chomp) end } end end end end