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
  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.

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.

3 Likes

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 = TracePoint.new(:c_call) { |tp| puts "#{tp.method_id} returns #{tp.return_value}" }

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

… poops out with

Error: 
#<ArgumentError: specified target is not supported>