Is this a useful pattern? (SketchUp extension automatic tests)

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

  yield call_info
  # Restore original method
  callee.singleton_class.alias_method(method_name, backup_method)

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")

# 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|
  puts "Expecting false: #{call_info[0]}"

  puts "Expecting true: #{call_info[0]}"
  puts "Expecting [\"Number is even\"]: #{call_info[1]}"

# 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."
    "The user says no."

# Simulate the user pressing yes
override_method(UI, :messagebox, IDYES) do |call_info|
  puts "Expecting: \"The user says yes.\""
  puts "Got: #{ask_user.inspect}"
# Simulate the user pressing no
override_method(UI, :messagebox, IDNO) do |call_info|
  puts "Expecting: \"The user says no.\""
  puts "Got: #{ask_user.inspect}"

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.

1 Like

Yes, this is called mocking. Useful for isolating your tests from other complex systems. Such as avoiding UI interaction or making real HTTP requests.


Yes, it looks interesting.

We can see when a method is called with TracePoint but cannot intercept or override for automation testing.

And for some reason targeting API module methods is unsupported. IE …

trace = { |tp| puts "#{tp.method_id} returns #{tp.return_value}" }

trace.enable(target: UI.method(:messagebox)) do
  UI.messagebox("This is a test")

… poops out with

#<ArgumentError: specified target is not supported>