class Object

The meta-programming that allows us to pop methods on and off for mocking

Public Instance Methods

class_def(name, &block) click to toggle source

Defines an instance method within a class

# File lib/motion-spec/mock/method_bind_unbind.rb, line 27
def class_def(name, &block)
  class_eval { define_method name, &block }
end
expanded_file_path(file) click to toggle source
# File lib/motion-spec.rb, line 21
def expanded_file_path(file)
  File.expand_path("../motion-spec/#{file}.rb", __FILE__)
end
false?() click to toggle source
# File lib/motion-spec/extensions/boolean.rb, line 6
def false?
  false
end
meta_def(name, &method_body) click to toggle source

Adds methods to a metaclass

# File lib/motion-spec/mock/method_bind_unbind.rb, line 15
def meta_def(name, &method_body)
  meta_eval do
    define_method(name) { |*args, &block| method_body.call(*args, &block) }
  end
end
meta_eval(&block) click to toggle source
# File lib/motion-spec/mock/method_bind_unbind.rb, line 10
def meta_eval(&block)
  metaclass.instance_eval(&block)
end
metaclass() click to toggle source

The hidden singleton lurks behind everyone

# File lib/motion-spec/mock/method_bind_unbind.rb, line 4
def metaclass
  class << self
    self
  end
end
mock!(method, options = {}) { |options| ... } click to toggle source

Create a mock method on an object. A mock object will place an expectation on behavior and cause a test failure if it’s not fulfilled.

Examples

my_string = "a wooden rabbit"
my_string.mock!(:retreat!, :return => "run away!  run away!")
my_string.mock!(:question, :return => "what is the airspeed velocity of an unladen sparrow?")

# test/your_test.rb
my_string.retreat!    # => "run away!  run away!"
# If we let the test case end at this point, it fails with:
# Unmet expectation: #<Sparrow:1ee7> expected question
# File lib/motion-spec/mock/mock.rb, line 16
def mock!(method, options = {}, &block)
  MotionSpec::Mocks.add([self, method])

  behavior =
    if block_given?
      lambda do |*args|
        fail ArgumentError if block.arity >= 0 && args.length != block.arity

        MotionSpec::Mocks.verify([self, method])
        block.call(*args)
      end
    elsif !options[:yield].nil?
      lambda do |*_args|
        MotionSpec::Mocks.verify([self, method])
        yield(options[:yield])
      end
    else
      lambda do |*_args|
        MotionSpec::Mocks.verify([self, method])
        return options[:return]
      end
    end

  safe_meta_def method, &behavior
end
proxy!(method, options = {}, &block) click to toggle source

Creates a proxy method on an object. In this setup, it places an expectation on an object (like a mock) but still calls the original method. So if you want to make sure the method is called and still return its value, or simply want to invoke the side effects of a method and return a stubbed value, then you can do that.

Examples

class Parrot
  def speak!
    puts @words
  end

  def say_this(words)
    @words = words
    "I shall say #{words}!"
  end
end

# => test/your_test.rb
sqawky = Parrot.new
sqawky.proxy!(:say_this)
# Proxy method still calls original method...
sqawky.say_this("hey")   # => "I shall say hey!"
sqawky.speak!            # => "hey"

sqawky.proxy!(:say_this, "herro!")
# Even though we return a stubbed value...
sqawky.say_this("these words")   # => "herro!"
# ...the side effects are still there.
sqawky.speak!                    # => "these words"

TODO: This implementation is still very rough. Needs refactoring and refining. Won’t work on ActiveRecord attributes, for example.

# File lib/motion-spec/mock/proxy.rb, line 36
def proxy!(method, options = {}, &block)
  MotionSpec::Mocks.add([self, method])

  if respond_to?(method)
    proxy_existing_method(method, options, &block)
  else
    proxy_missing_method(method, options, &block)
  end
end
require_lib_files(files) click to toggle source

Proper load order of all the classes/modules

# File lib/motion-spec.rb, line 9
def require_lib_files(files)
  case files
  when String
    file_paths = expanded_file_path(files)
    file_paths = Dir.glob(file_paths) if files.include? '*'
  when Array
    file_paths = files.map { |f| expanded_file_path(f) }
  end

  Motion::Require.all(file_paths)
end
reset(method_name) click to toggle source
# File lib/motion-spec/mock/method_bind_unbind.rb, line 31
def reset(method_name)
  metaclass.restore_original_method(method_name)
end
safe_meta_def(name, &method_body) click to toggle source
# File lib/motion-spec/mock/method_bind_unbind.rb, line 21
def safe_meta_def(name, &method_body)
  metaclass.remember_original_method(name)
  meta_def(name, &method_body)
end
should(*args, &block) click to toggle source
# File lib/motion-spec/extensions/object.rb, line 2
def should(*args, &block)
  MotionSpec::Should.new(self).be(*args, &block)
end
should_not_call(method) click to toggle source
# File lib/motion-spec/mock/mock.rb, line 42
def should_not_call(method)
  safe_meta_def(method) do
    should.flunk "Umet expectations: #{method} expected to not be called"
  end
end
stub!(method_name, options = {}, &stubbed) click to toggle source

Create a stub method on an object. Simply returns a value for a method call on an object.

Examples

my_string = "a wooden rabbit"
my_string.stub!(:retreat!, :return => "run away!  run away!")

# test/your_test.rb
my_string.retreat!    # => "run away!  run away!"
# File lib/motion-spec/mock/stub.rb, line 13
def stub!(method_name, options = {}, &stubbed)
  MotionSpec::Stubs.add(self, method_name)

  behavior = (block_given? ? stubbed : -> { return options[:return] })

  safe_meta_def method_name, &behavior
end
true?() click to toggle source
# File lib/motion-spec/extensions/boolean.rb, line 2
def true?
  false
end

Protected Instance Methods

motion_spec_original_method_name(method_name) click to toggle source
# File lib/motion-spec/mock/method_bind_unbind.rb, line 37
def motion_spec_original_method_name(method_name)
  "__original_#{method_name}".to_sym
end
proxy_existing_method(method, options = {}, &_block) click to toggle source
# File lib/motion-spec/mock/proxy.rb, line 48
def proxy_existing_method(method, options = {}, &_block)
  method_alias = "__old_#{method}".to_sym

  meta_eval { module_eval { alias_method method_alias, method } }

  check_arity = proc do |args|
    arity = method(method_alias.to_sym).arity

    # Negative arity means some params are optional, so we check for the
    # minimum required. Sadly, we can't tell what the maximum is.
    has_mimimum_arguments =
      if arity >= 0
        args.length != arity
      else
        args.length < ~arity
      end

    fail ArgumentError unless has_mimimum_arguments
  end

  default_operation =
    lambda do |*args|
      check_arity.call(args)

      MotionSpec::Mocks.verify([self, method])

      if method(method_alias.to_sym).arity == 0
        send(method_alias)
      else
        send(method_alias, *args)
      end
    end

  behavior =
    if options[:return]
      lambda do |*args|
        default_operation.call(*args)
        return options[:return]
      end
    else
      default_operation
    end

  meta_def method, &behavior
end
proxy_missing_method(method, options = {}, &_block) click to toggle source
# File lib/motion-spec/mock/proxy.rb, line 94
def proxy_missing_method(method, options = {}, &_block)
  default_operation =
    lambda do |*args|
      MotionSpec::Mocks.verify([self, method])

      method_missing(method, args)
    end

  behavior =
    if options[:return]
      lambda do |*args|
        default_operation.call(*args)
        return options[:return]
      end
    else
      default_operation
    end

  meta_def method, &behavior
end
remember_original_method(method_name) click to toggle source
# File lib/motion-spec/mock/method_bind_unbind.rb, line 41
def remember_original_method(method_name)
  if method_defined?(method_name)
    alias_method motion_spec_original_method_name(method_name), method_name
  end
  self
end
restore_original_method(method_name) click to toggle source
# File lib/motion-spec/mock/method_bind_unbind.rb, line 48
def restore_original_method(method_name)
  original_method_name = motion_spec_original_method_name(method_name)
  if method_defined?(original_method_name)
    alias_method method_name, original_method_name
    remove_method original_method_name
  end
  self
end