# Stubbing support # # Stubs methods on classes and instances # # Why's "metaid.rb" stuff crunched down: class Object #:nodoc:# def hm_metaclass #:nodoc:# class << self self end end def hm_meta_eval(&blk) #:nodoc:# hm_metaclass.instance_eval(&blk) end def hm_meta_def(name, &blk) #:nodoc:# hm_meta_eval { define_method name, &blk } end end module Hardmock # == Hardmock: Stubbing and Mocking Concrete Methods # # Hardmock lets you stub and/or mock methods on concrete classes or objects. # # * To "stub" a concrete method is to rig it to return the same thing always, disregarding any arguments. # * To "mock" a concrete method is to surplant its funcionality by delegating to a mock object who will cover this behavior. # # Mocked methods have their expectations considered along with all other mock object expectations. # # If you use stubbing or concrete mocking in the absence (or before creation) of other mocks, you need to invoke prepare_hardmock_control. # Once verify_mocks or clear_expectaions is called, the overriden behavior in the target objects is restored. # # == Examples # # River.stubs!(:sounds_like).returns("gurgle") # # River.expects!(:jump).returns("splash") # # rogue.stubs!(:sounds_like).returns("pshshsh") # # rogue.expects!(:rawhide_tanning_solvents).returns("giant snapping turtles") # module Stubbing # Exists only for documentation end class ReplacedMethod #:nodoc:# attr_reader :target, :method_name def initialize(target, method_name) @target = target @method_name = method_name Hardmock.track_replaced_method self end end class StubbedMethod < ReplacedMethod #:nodoc:# def invoke(args) raise @raises if @raises @return_value end def returns(stubbed_return) @return_value = stubbed_return end def raises(err) err = RuntimeError.new(err) unless err.kind_of?(Exception) @raises = err end end class ::Object def stubs!(method_name) method_name = method_name.to_s already_stubbed = Hardmock.has_replaced_method?(self, method_name) stubbed_method = Hardmock::StubbedMethod.new(self, method_name) unless _is_mock? or already_stubbed if methods.include?(method_name.to_s) hm_meta_eval do alias_method "_hardmock_original_#{method_name}".to_sym, method_name.to_sym end end end hm_meta_def method_name do |*args| stubbed_method.invoke(args) end stubbed_method end def expects!(method_name, *args, &block) if self._is_mock? raise Hardmock::StubbingError, "Cannot use 'expects!(:#{method_name})' on a Mock object; try 'expects' instead" end method_name = method_name.to_s @_my_mock = Mock.new(_my_name, $main_mock_control) if @_my_mock.nil? unless Hardmock.has_replaced_method?(self, method_name) # Track the method as replaced Hardmock::ReplacedMethod.new(self, method_name) # Preserver original implementation of the method by aliasing it away if methods.include?(method_name) hm_meta_eval do alias_method "_hardmock_original_#{method_name}".to_sym, method_name.to_sym end end # Re-define the method to utilize our patron mock instance. # (This global-temp-var thing is hokey but I was having difficulty generating # code for the meta class.) begin $method_text_temp = %{ def #{method_name}(*args,&block) @_my_mock.__send__(:#{method_name}, *args, &block) end } class << self eval $method_text_temp end ensure $method_text_temp = nil end end return @_my_mock.expects(method_name, *args, &block) end def _is_mock? self.kind_of?(Mock) end def _my_name self.kind_of?(Class) ? self.name : self.class.name end def _clear_mock @_my_mock = nil end end class ::NilClass # Use this only if you really mean it alias_method :intentionally_stubs!, :stubs! # Use this only if you really mean it alias_method :intentionally_expects!, :expects! # Overridden to protect against accidental nil reference self delusion def stubs!(mname) raise StubbingError, "Cannot stub #{mname} method on nil. (If you really mean to, try 'intentionally_stubs!')" end # Overridden to protect against accidental nil reference self delusion def expects!(mname, *args) raise StubbingError, "Cannot mock #{mname} method on nil. (If you really mean to, try 'intentionally_expects!')" end end class << self def track_replaced_method(replaced_method) all_replaced_methods << replaced_method end def all_replaced_methods $all_replaced_methods ||= [] end def has_replaced_method?(obj, method_name) hits = all_replaced_methods.select do |replaced| (replaced.target.object_id == obj.object_id) and (replaced.method_name.to_s == method_name.to_s) end return !hits.empty? end def restore_all_replaced_methods all_replaced_methods.each do |replaced| unless replaced.target._is_mock? backed_up = "_hardmock_original_#{replaced.method_name}" if replaced.target.methods.include?(backed_up) replaced.target.hm_meta_eval do alias_method replaced.method_name.to_sym, backed_up.to_sym end end replaced.target._clear_mock end end all_replaced_methods.clear end end end