Last active
June 2, 2022 10:27
-
-
Save Overbryd/b4ea6ec28f4ff9d2a65f to your computer and use it in GitHub Desktop.
Sexiest assertion since I started testing APIs: assert_structure
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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