class Object
The meta-programming that allows us to pop methods on and off for mocking
Public Instance Methods
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
# File lib/motion-spec.rb, line 21 def expanded_file_path(file) File.expand_path("../motion-spec/#{file}.rb", __FILE__) end
# File lib/motion-spec/extensions/boolean.rb, line 6 def false? false end
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
# File lib/motion-spec/mock/method_bind_unbind.rb, line 10 def meta_eval(&block) metaclass.instance_eval(&block) end
The hidden singleton lurks behind everyone
# File lib/motion-spec/mock/method_bind_unbind.rb, line 4 def metaclass class << self self end end
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
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
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
# File lib/motion-spec/mock/method_bind_unbind.rb, line 31 def reset(method_name) metaclass.restore_original_method(method_name) end
# 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
# File lib/motion-spec/extensions/object.rb, line 2 def should(*args, &block) MotionSpec::Should.new(self).be(*args, &block) end
# 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
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
# File lib/motion-spec/extensions/boolean.rb, line 2 def true? false end
Protected Instance Methods
# 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
# 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
# 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
# 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
# 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