Skip to content

Instantly share code, notes, and snippets.

@Overbryd
Last active June 2, 2022 10:27
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Overbryd/b4ea6ec28f4ff9d2a65f to your computer and use it in GitHub Desktop.
Save Overbryd/b4ea6ec28f4ff9d2a65f to your computer and use it in GitHub Desktop.
Sexiest assertion since I started testing APIs: assert_structure
# lets say this is the response we receive
response = {
"results" => {
"total_count" => 15,
"per_page" => 100,
"companies" => [
{
"company" => {
"name" => "Foo Bar Ltd",
"registered_address" => { ... },
...
}
},
{
"company" => {
"name" => "Zig Zag Inc",
"registered_address" => { ... },
...
}
},
{
"company" => {
"name" => "Meh Bleh",
"registered_address" => { ... },
# BOOM! We expect each object in this array not have a registered_address_in_full
"registered_address_in_full" => "Foobargl",
...
}
}
...
]
}
}
assert_structure({
"results" => { # assert there is a results hash
"companies" => [ # with a key 'companies' holding an array
{ # each element being a hash
"company" => { # with a key 'company' holding a hash
"name" => String, # with a key 'name' of type String
"registered_address_in_full" => nil # without a key 'registered_address_in_full'
}
}
]
}
}, response)
# This will give super nice and awesome error messages like:
#
# Structure does not match in: { "results" => { "companies" => [ 2 => { "company" => { "registered_address_in_full" => equal(nil).
#
# HELL YEA!
# Now you can exactly understand where and what broke!
module MiniTest::Assertions
# assert_structure(expectation, object)
# will inspect the given object (second argument) and match with the structure definition (first argument).
# As structure a Hash or an Array can be supplied
# Hash will be matched for their keys and values
# Arrays will be matched for being an array and optionally traversed so that each element matches a given structure
#
# This _is_ the holy grail of API response testing.
#
# Possible values for any given structure:
# :_something_ - can be used as a non-nil wildcard, useful for checking the existance of a key
# nil - can be used as not existing, useful for checking if a key is set or not
# /regexp/ - useful for checking on parts of the values
# any class - can be used for asserting the type
# any value - can be used for asserting the actual value
# lambda - can be used for your own code, will be given the value and expects a boolean return value
def assert_structure(expectation, object)
stack = [[expectation, object, ""]]
until (expectation, object, path = *stack.pop).empty? do
case expectation
when :_something_
path << "something"
refute_equal(nil, object, "Structure does not match in: #{path}")
when Regexp
path << "match(#{expectation.inspect})"
assert_kind_of(String, object, "Structure does not match in: #{path}")
assert_match(expectation, object, "Structure does not match in: #{path}")
when Proc
path << "#{expectation.lambda? ? "lambda" : "proc"}"
assert(expectation.call(object), "Structure does not match in: #{path}")
when Hash
path << "{ "
assert_kind_of(Hash, object, "Structure does not match in: #{path}")
expectation.each do |key, expected|
stack << [expected, object[key], path + "#{key.inspect} => "]
end
when Array
path << "[ "
assert_kind_of(Array, object)
next unless expectation = expectation.first
refute_empty(object, "Structure does not match in: #{path}")
object.each_with_index do |element, index|
stack << [expectation, element, path + "#{index} => "]
end
when Class
path << "kind_of(#{expectation.name})"
assert_kind_of(expectation, object, "Structure does not match in: #{path}")
else
path << "equal(#{expectation.inspect})"
assert_equal(expectation, object, "Structure does not match in: #{path}")
end
end
end
end
module MiniTest::Assertions
# assert_xml_structure(expectation, xml)
# will inspect the given object or string (second argument) and match with the structure definition (first argument)
# As structure a hash should be supplied
# The hash will be traversed, each key is a XPATH expression, each value either an expectation or another nested structure
#
# Possible values:
# :_something_ - can be used as a non-nil wildcard, useful for checking the existance of a key
# [] - can be used as not existing, useful for checking if an XPATH returns an empty array
# /regexp/ - useful for checking on text of the tested node
# String - can be used for asserting some non blank text is set at the node
# "string value" - can be used for asserting a nodes text is equal to the given string
# [{structure}] - an array with one structure definition can be used for asserting the structure of the children nodes
# lambda - can be used for your own code, will be given the value and expects a boolean return value
def assert_xml_structure(expectation, xml)
xml = Nokogiri.XML(xml) { |config| config.noblanks } if xml.is_a?(String)
stack = [[expectation, xml, ""]]
until (expectation, xml, path = *stack.pop).empty? do
case expectation
when :_something_
path << "something"
refute_equal(nil, xml.empty?, "Structure does not match in: #{path}")
when Regexp
path << "match(#{expectation.inspect})"
assert_match(expectation, xml.text, "Structure does not match in: #{path}")
when Proc
path << "#{expectation.lambda? ? "lambda" : "proc"}"
assert(expectation.call(xml), "Structure does not match in: #{path}")
when Hash
path << "{ "
expectation.each do |key, expected|
stack << [expected, xml.xpath(key), path + "#{key} => "]
end
when Array
path << "[ "
if structure = expectation.first
xml.children.each_with_index do |child, index|
stack << [expectation, child, path + "#{index} => "]
end
else
assert_empty(xml, "Structure does not match in: #{path}")
end
when Class
if expectation == String
path << "string"
refute(xml.text.blank?, "Structure does not match in: #{path}")
end
when String
path << "equal(#{expectation.inspect})"
assert_equal(expectation, xml.text, "Structure does not match in: #{path}")
else
path << "equal(#{expectation.inspect})"
assert_equal(expectation, xml, "Structure does not match in: #{path}")
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment