require 'mocha/mockery' require 'mocha/instance_method' require 'mocha/class_method' require 'mocha/module_method' require 'mocha/any_instance_method' require 'mocha/argument_iterator' require 'mocha/expectation_error_factory' module Mocha # Methods added to all objects to allow mocking and stubbing on real (i.e. non-mock) objects. # # Both {#expects} and {#stubs} return an {Expectation} which can be further modified by methods on {Expectation}. module ObjectMethods # @private alias_method :_method, :method # @private def mocha @mocha ||= Mocha::Mockery.instance.mock_impersonating(self) end # @private def reset_mocha @mocha = nil end # @private def stubba_method Mocha::InstanceMethod end # @private def stubba_object self end # Adds an expectation that the specified method must be called exactly once with any parameters. # # The original implementation of the method is replaced during the test and then restored at the end of the test. # # @param [Symbol,String] method_name name of expected method # @param [Hash] expected_methods_vs_return_values expected method name symbols as keys and corresponding return values as values - these expectations are setup as if {#expects} were called multiple times. # # @overload def expects(method_name) # @overload def expects(expected_methods_vs_return_values) # @return [Expectation] last-built expectation which can be further modified by methods on {Expectation}. # @raise [StubbingError] if attempting to stub method which is not allowed. # # @example Setting up an expectation on a non-mock object. # product = Product.new # product.expects(:save).returns(true) # assert_equal true, product.save # # @example Setting up multiple expectations on a non-mock object. # product = Product.new # product.expects(:valid? => true, :save => true) # # # exactly equivalent to # # product = Product.new # product.expects(:valid?).returns(true) # product.expects(:save).returns(true) # # @see Mock#expects def expects(expected_methods_vs_return_values) if expected_methods_vs_return_values.to_s =~ /the[^a-z]*spanish[^a-z]*inquisition/i raise ExpectationErrorFactory.build('NOBODY EXPECTS THE SPANISH INQUISITION!') end if frozen? raise StubbingError.new("can't stub method on frozen object: #{mocha_inspect}", caller) end expectation = nil mockery = Mocha::Mockery.instance iterator = ArgumentIterator.new(expected_methods_vs_return_values) iterator.each { |*args| method_name = args.shift mockery.on_stubbing(self, method_name) method = stubba_method.new(stubba_object, method_name) mockery.stubba.stub(method) expectation = mocha.expects(method_name, caller) expectation.returns(args.shift) if args.length > 0 } expectation end # Adds an expectation that the specified method may be called any number of times with any parameters. # # @param [Symbol,String] method_name name of stubbed method # @param [Hash] stubbed_methods_vs_return_values stubbed method name symbols as keys and corresponding return values as values - these stubbed methods are setup as if {#stubs} were called multiple times. # # @overload def stubs(method_name) # @overload def stubs(stubbed_methods_vs_return_values) # @return [Expectation] last-built expectation which can be further modified by methods on {Expectation}. # @raise [StubbingError] if attempting to stub method which is not allowed. # # @example Setting up a stubbed methods on a non-mock object. # product = Product.new # product.stubs(:save).returns(true) # assert_equal true, product.save # # @example Setting up multiple stubbed methods on a non-mock object. # product = Product.new # product.stubs(:valid? => true, :save => true) # # # exactly equivalent to # # product = Product.new # product.stubs(:valid?).returns(true) # product.stubs(:save).returns(true) # # @see Mock#stubs def stubs(stubbed_methods_vs_return_values) if frozen? raise StubbingError.new("can't stub method on frozen object: #{mocha_inspect}", caller) end expectation = nil mockery = Mocha::Mockery.instance iterator = ArgumentIterator.new(stubbed_methods_vs_return_values) iterator.each { |*args| method_name = args.shift mockery.on_stubbing(self, method_name) method = stubba_method.new(stubba_object, method_name) mockery.stubba.stub(method) expectation = mocha.stubs(method_name, caller) expectation.returns(args.shift) if args.length > 0 } expectation end # Removes the specified stubbed methods (added by calls to {#expects} or {#stubs}) and all expectations associated with them. # # Restores the original behaviour of the methods before they were stubbed. # # WARNING: If you {#unstub} a method which still has unsatisfied expectations, you may be removing the only way those expectations can be satisfied. Use {#unstub} with care. # # @param [Array] method_names names of methods to unstub. # # @example Stubbing and unstubbing a method on a real (non-mock) object. # multiplier = Multiplier.new # multiplier.double(2) # => 4 # multiplier.stubs(:double).raises # new behaviour defined # multiplier.double(2) # => raises exception # multiplier.unstub(:double) # original behaviour restored # multiplier.double(2) # => 4 # # @example Unstubbing multiple methods on a real (non-mock) object. # multiplier.unstub(:double, :triple) # # # exactly equivalent to # # multiplier.unstub(:double) # multiplier.unstub(:triple) def unstub(*method_names) mockery = Mocha::Mockery.instance method_names.each do |method_name| method = stubba_method.new(stubba_object, method_name) mockery.stubba.unstub(method) end end # @private def method_exists?(method, include_public_methods = true) if include_public_methods return true if public_methods(include_superclass_methods = true).include?(method) return true if respond_to?(method.to_sym) end return true if protected_methods(include_superclass_methods = true).include?(method) return true if private_methods(include_superclass_methods = true).include?(method) return false end end # @private module ModuleMethods def stubba_method Mocha::ModuleMethod end end # Methods added to all classes to allow mocking and stubbing on real (i.e. non-mock) objects. module ClassMethods # @private def stubba_method Mocha::ClassMethod end # @private class AnyInstance def initialize(klass) @stubba_object = klass end def mocha @mocha ||= Mocha::Mockery.instance.mock_impersonating_any_instance_of(@stubba_object) end def stubba_method Mocha::AnyInstanceMethod end def stubba_object @stubba_object end def method_exists?(method, include_public_methods = true) if include_public_methods return true if @stubba_object.public_instance_methods(include_superclass_methods = true).include?(method) end return true if @stubba_object.protected_instance_methods(include_superclass_methods = true).include?(method) return true if @stubba_object.private_instance_methods(include_superclass_methods = true).include?(method) return false end end # @return [Mock] a mock object which will detect calls to any instance of this class. # @raise [StubbingError] if attempting to stub method which is not allowed. # # @example Return false to invocation of +Product#save+ for any instance of +Product+. # Product.any_instance.stubs(:save).returns(false) # product_1 = Product.new # assert_equal false, product_1.save # product_2 = Product.new # assert_equal false, product_2.save def any_instance if frozen? raise StubbingError.new("can't stub method on frozen object: #{mocha_inspect}.any_instance", caller) end @any_instance ||= AnyInstance.new(self) end end end # @private class Object include Mocha::ObjectMethods end # @private class Module include Mocha::ModuleMethods end # @private class Class include Mocha::ClassMethods end