class Spy

Constants

THREAD_LOCAL_ACTIVE_SPIES_KEY
VERSION

Attributes

all_instances[R]
obj[R]

@return [Object] the instance, class, or module being spied on by this spy

spied_methods_map[R]

Public Class Methods

active_spies() click to toggle source

Active spies in the current thread.

@return [Array<Spy>] instances of `Spy` that have active method spies on

their target obj
# File lib/spy.rb, line 15
def self.active_spies
  Thread.current[THREAD_LOCAL_ACTIVE_SPIES_KEY] ||= []
end
clean() { || ... } click to toggle source

Remove all active Spy instances in the current thread and restore their spied methods to the original state.

@yield Optionally provide a block, and spies created within the block will

be cleaned, leaving the outer scope untouched. Blocks can be nested.
# File lib/spy.rb, line 71
def self.clean
  if block_given?
    outer_active_spies = active_spies
    Thread.current[THREAD_LOCAL_ACTIVE_SPIES_KEY] = nil

    begin
      yield
    ensure
      clean
      Thread.current[THREAD_LOCAL_ACTIVE_SPIES_KEY] = outer_active_spies
    end
  else
    active_spies.dup.each do |spy|
      rspec_mock?(spy) ? unregister(spy) : spy.clean
    end
  end
end
new(obj, all_instances: false) click to toggle source
# File lib/spy.rb, line 96
def initialize(obj, all_instances: false)
  @obj = obj
  @calls = []
  @all_instances = all_instances
  @spied_methods_map = {}
end
on(obj, method_name = nil) click to toggle source

Spy on an instance, class, or module.

By default, this will spy on all user-defined methods (not methods defined on Ruby's base Class).

@param obj [Object] the instance, class, or module to spy on @param method_name [Symbol] optionally limit spying to a single method

@return [Spy] an active instance of `Spy` for the given `obj`

# File lib/spy.rb, line 38
def self.on(obj, method_name = nil)
  spy_with_options(obj, method_name)
end
on_all_instances_of(obj, method_name = nil) click to toggle source

Spy on all instances of a class.

By default, this will spy on all user-defined methods (not methods defined on Ruby's base Class).

@param obj [Object] the class to spy on @param method_name [Symbol] optionally limit spying to a single method

@return [Spy] an active instance of `Spy` for the given `obj`

# File lib/spy.rb, line 53
def self.on_all_instances_of(obj, method_name = nil)
  spy_with_options(obj, method_name, all_instances: true)
end
register(spy) click to toggle source
# File lib/spy.rb, line 19
def self.register(spy)
  active_spies << spy unless active_spies.include? spy
end
rspec_mock?(obj) click to toggle source
# File lib/spy.rb, line 89
def self.rspec_mock?(obj)
  defined?(RSpec::Mocks::Double) && obj.is_a?(RSpec::Mocks::Double)
end
spy_with_options(obj, method_name, options = {}) click to toggle source
# File lib/spy.rb, line 57
def self.spy_with_options(obj, method_name, options = {})
  new(obj, options).tap do |spy|
    method_name ? spy.on(method_name) : spy.on_all
    Spy.register(spy)
  end
end
unregister(spy) click to toggle source
# File lib/spy.rb, line 23
def self.unregister(spy)
  active_spies.delete(spy)
end

Public Instance Methods

calls(method_name = nil) click to toggle source

Information about the calls received by this spy

@param method_name [Symbol] optionally filter results to the given method

@return [Array<Call>] set of `Call` objects containing information about

each method call since the spy was activated.
# File lib/spy.rb, line 135
def calls(method_name = nil)
  if method_name
    @calls.select { |c| c.method_name == method_name }
  else
    @calls
  end
end
clean() click to toggle source

Remove spy and return spied methods on `obj` to the original state.

@return [Spy] self

# File lib/spy.rb, line 148
def clean
  spied_methods_map.keys.each { |m| remove_spy(m) }
  Spy.unregister(self)
  self
end
dirty?() click to toggle source

Check if spy is actively spying on any methods on `obj`

@return [Boolean] dirty state

# File lib/spy.rb, line 159
def dirty?
  spied_methods_map.keys.any?
end
on(method_name) click to toggle source

Spy on a single method on `obj`

@param method_name [Symbol] the method to spy on

@return [Spy] self

# File lib/spy.rb, line 121
def on(method_name)
  spy_on(method_name)
  Spy.register(self)
  self
end
on_all() click to toggle source

Spy on all user-defined methods on `obj`

@return [Spy] self

# File lib/spy.rb, line 108
def on_all
  all_methods.each { |m| spy_on(m) }
  Spy.register(self)
  self
end

Private Instance Methods

all_methods() click to toggle source
# File lib/spy.rb, line 167
def all_methods
  if spying_on_class?
    obj.methods - Class.methods
  elsif spying_on_all_instances?
    obj.instance_methods - Class.instance_methods
  else
    obj.methods - Class.instance_methods
  end
end
original_method_name(method_name) click to toggle source
# File lib/spy.rb, line 223
def original_method_name(method_name)
  spied_methods_map[method_name] ||= loop do
    name_candidate = "#{method_name}_#{SecureRandom.hex(8)}".to_sym
    break name_candidate unless all_methods.include? name_candidate
  end
end
remove_spy(method_name) click to toggle source
# File lib/spy.rb, line 201
def remove_spy(method_name)
  aliased_original_method_name = original_method_name(method_name)

  target_obj.send(
    :alias_method,
    method_name,
    aliased_original_method_name
  )

  target_obj.send(:remove_method, aliased_original_method_name)

  spied_methods_map.delete method_name
end
singleton_class() click to toggle source
# File lib/spy.rb, line 177
def singleton_class
  class << obj; self; end
end
spy_on(method_name) click to toggle source
# File lib/spy.rb, line 185
def spy_on(method_name)
  spy = self
  aliased_original_method_name = original_method_name(method_name)

  target_obj.send(
    :alias_method,
    aliased_original_method_name,
    method_name
  )

  target_obj.send(:define_method, method_name) do |*args, &block|
    spy.calls << Call.new(self, method_name, args, block)
    send aliased_original_method_name, *args, &block
  end
end
spying_on_all_instances?() click to toggle source
# File lib/spy.rb, line 219
def spying_on_all_instances?
  !!all_instances
end
spying_on_class?() click to toggle source
# File lib/spy.rb, line 215
def spying_on_class?
  obj.class == Class && !spying_on_all_instances?
end
target_obj() click to toggle source
# File lib/spy.rb, line 181
def target_obj
  spying_on_all_instances? ? obj : singleton_class
end