I’ve been tinkering with TestUp tests for my extension and some voodoo meta programming, and discovered/invented this little pattern.
# Intersect calls to a method.
#
# Used to test if the method was called, and if so, with what arguments.
#
# @param callee [Object]
# @param method_name [Symbol]
#
# @yieldparam call_info [Array(Boolean, [Array, nil])
# Whether method was called, the arguments it was called with.
def override_method(callee, method_name, return_value = nil)
# Ruby arrays are always passed as reference.
# yield is called before the body of the overridden method,
# but the values of the yielded array can be accessed after.
call_info = [false, nil]
# Backup original method
backup_method = :backupped_method_temporary_name
callee.singleton_class.alias_method(backup_method, method_name)
callee.singleton_class.define_method(method_name) do |*arguments|
puts "Intercepted method call (#{callee}.#{method_name.to_s})"
call_info[0] = true
call_info[1] = arguments
# REVIEW: Optionally forward to original method.
# May want to forward to View#drawing=color, but not UI.inputbox
next return_value
end
yield call_info
ensure
# Restore original method
callee.singleton_class.alias_method(method_name, backup_method)
end
With the above method you can temporarily override a method, to test if some code is calling it and if so with what argument.
# Subject method
# Expected to show an inputbox if called with an even number, but does it?
def alert_on_even(number)
return unless number.even?
UI.messagebox("Number is even")
end
# Our test
# Runs in automation; we don't want the modal inputbox to halt the execution.
# (replace puts with assertions)
override_method(UI, :messagebox) do |call_info|
alert_on_even(1)
puts "Expecting false: #{call_info[0]}"
alert_on_even(2)
puts "Expecting true: #{call_info[0]}"
puts "Expecting [\"Number is even\"]: #{call_info[1]}"
end
# Check that UI.messagebox was restored when block ended
UI.messagebox("This is expected to be shown in a messagebox")
You can also simulate a return value from the intercepted method.
def ask_user
result = UI.messagebox("Is this a rhetorical question?", MB_YESNO)
if result == IDYES
"The user says yes."
else
"The user says no."
end
end
# Simulate the user pressing yes
override_method(UI, :messagebox, IDYES) do |call_info|
puts "Expecting: \"The user says yes.\""
puts "Got: #{ask_user.inspect}"
end
# Simulate the user pressing no
override_method(UI, :messagebox, IDNO) do |call_info|
puts "Expecting: \"The user says no.\""
puts "Got: #{ask_user.inspect}"
end
Could this be a useful pattern? The obvious thing to do is to extract as much logic as you can to small static methods that can be tested in a vacuum. However, sometimes you want to test how one thing interacts with another thing.
Typical use cases I’m thinking of, other than UI.messagebox and UI.inputbox, is testing if a custom Ruby tool sets the desired warning/invalid cursor after hovering (OnMouseMove) an entity it can’t interact with, or sets the View#drawing_color= to say red.